{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"],"fields":{"title":{"boost":1000.0},"text":{"boost":1.0},"tags":{"boost":1000000.0}}},"docs":[{"location":"","title":"Storyteller Swift SDK Documentation","text":"<p>Use Storyteller to add Stories, Clips, and Cards to your iOS or tvOS application. Choose the route that matches what you are trying to do.</p>"},{"location":"#quick-start","title":"Start Here","text":"<p>For a new iOS integration:</p> <ul> <li>Install the SDK.</li> <li>Show your first content with UIKit.</li> <li>Show your first content with SwiftUI.</li> </ul> <p>The UIKit and SwiftUI routes are both complete; choose the framework your app uses. For Apple TV, use the tvOS Guide.</p> <p>Using an AI coding assistant? The AI-assisted integration guide explains how to give it the published Storyteller documentation and, if you have access to the Storyteller iOS Showcase repository, use its <code>$integrate-storyteller</code> skill.</p>"},{"location":"#sdk-and-showcase-requirements","title":"SDK and Showcase Requirements","text":"<p>The SDK compatibility contract is separate from the build settings used by the Showcase applications. A deployment target determines which operating-system versions can run your app; a Showcase app's Xcode version and Swift language mode describe how that example project is built.</p> <p>The current Storyteller SDK supports these application deployment targets:</p> Platform Minimum deployment target iOS iOS 13.0 tvOS tvOS 15.0 <p>We test the current SDK line with Xcode 26.2. Earlier Xcode versions may also work, but they are not part of our tested configuration. The deployment targets above define the supported operating-system versions. The SDK binary is built in Swift 6, but it can be consumed by app targets using either Swift 5 or Swift 6 language mode.</p> <p>The private iOS Showcase repository contains two deliberately modern example applications:</p> Application Installation App deployment target Swift language mode Xcode used for testing SwiftUI Feature Showcase Swift Package Manager iOS 16.0 Swift 6 26.2 UIKit/CocoaPods Sample CocoaPods iOS 16.0 Swift 5 26.2 <p>Those are the checked-in example configurations, not higher minimums for the SDK. Your own app can keep an iOS 13 deployment target and its existing supported Swift language mode while adopting the current SDK.</p>"},{"location":"#showcase-source-access","title":"Access the iOS Showcase Source","text":"<p>The iOS documentation and the private Showcase source are both available to Storyteller customers, but access is granted separately. The documentation access code supplied by Storyteller does not grant GitHub access.</p> <p>To use the Showcase source links in these guides, tell your Storyteller contact which GitHub account you want authorised. Accept the resulting GitHub invitation, then make sure you are signed in with that exact account before opening a link.</p> <p>The Showcase repository intentionally returns a 404 to anyone without access. If a link returns a 404, check that you are signed in to GitHub with the authorised account, that you accepted the invitation, and that Storyteller has granted that account access. Contact support@getstoryteller.com if you still cannot open the source.</p>"},{"location":"#documentation","title":"Choose a Documentation Path","text":""},{"location":"#getting-started","title":"Set Up or Upgrade an Integration","text":"<p>Start with the iOS Quickstart Guide, follow the tvOS Guide, or migrate an existing integration from version 10.</p>"},{"location":"#components","title":"Add a Storyteller Experience","text":"<p>Choose Story and Clip Lists, Storyteller Home, Embedded Clips, or Storyteller Cards. You can also open a Player programmatically or add content Search.</p>"},{"location":"#configuration","title":"Configure Your Integration","text":"<p>Set up user identity and personalization, choose privacy and tracking behavior, and customize Storyteller themes.</p>"},{"location":"#features","title":"Add Features and Integrations","text":"<p>Continue with Analytics integration, the Analytics Event Reference, Ads, Deep Linking, navigation back into your app, or Brightcove.</p>"},{"location":"#reference","title":"Look Up SDK Behavior","text":"<p>Use Delegates and Callbacks, Storyteller Modules, or Additional SDK Methods for API details. You can also use the documentation with AI assistants or review the release notes.</p>"},{"location":"#support","title":"Troubleshoot or Get Help","text":"<p>Start with Troubleshooting Storyteller on iOS to identify the failing boundary and the next useful check. If you still need help, contact us at support@getstoryteller.com.</p>"},{"location":"AI/","title":"Using Storyteller SDK Docs with AI Assistants","text":"<p>This guide explains how to use Storyteller SDK documentation with AI-powered coding assistants to enhance your development experience.</p>"},{"location":"AI/#why-use-ai-assistants-with-documentation","title":"Why Use AI Assistants with Documentation","text":"<p>AI coding assistants can significantly improve your development workflow when integrating the Storyteller SDK. By providing your tool with our documentation, you can:</p> <ul> <li>Get contextually relevant code suggestions</li> <li>Receive guidance based on Storyteller best practices</li> <li>Accelerate your implementation process</li> <li>Troubleshoot issues more effectively</li> </ul>"},{"location":"AI/#use-the-showcase-integration-skill","title":"Use the Showcase Integration Skill","text":"<p>If you have access to the private Storyteller iOS Showcase repository, it includes an <code>$integrate-storyteller</code> skill at <code>.agents/skills/integrate-storyteller/SKILL.md</code> for AI coding assistants that support repository-local skills. It can guide a new integration, audit an existing integration, or help troubleshoot a problem using your app and the published Storyteller documentation.</p> <p>Open the Showcase repository in your assistant's workspace alongside your iOS app, then ask:</p> <p>Use <code>$integrate-storyteller</code> to help me integrate Storyteller into <code>&lt;path-to-my-ios-app&gt;</code>.</p> <p>For an existing integration, ask the skill to audit the app instead. It will inspect your app as the primary source, ground its guidance in the published documentation, and identify assumptions explicitly.</p> <p>If you do not have access to the Showcase repository or your assistant does not support repository-local skills, use the documentation bundle below.</p>"},{"location":"AI/#adding-documentation-to-your-project","title":"Adding Documentation to Your Project","text":""},{"location":"AI/#download-llmstxt","title":"Download <code>llms.txt</code>","text":"<p>Download the Storyteller iOS documentation bundle and place it somewhere your editor can reference.</p> <p>The bundle can be saved into your local repository and then referenced by your AI tool:</p> <ol> <li>Create a <code>.ai/</code> directory in your project</li> <li>Save the downloaded content as <code>.ai/storyteller-sdk-docs.md</code></li> <li>Point your tool to <code>.ai/storyteller-sdk-docs.md</code> and reference it in your prompts, or paste the relevant sections into the editor</li> </ol> <p>For narrower context, read the aggregate bundle's <code>&lt;PAGE: slug&gt;</code> markers and request the corresponding concrete topic filename from the <code>/ios/ai/</code> directory. For example, <code>&lt;PAGE: quickstart&gt;</code> maps to the Quickstart topic bundle.</p> <p>Agents can also use the documentation search index to discover concrete documentation pages. Confirm important answers against those pages.</p>"},{"location":"AI/#example-prompts","title":"Example Prompts","text":"<p>Here are some effective prompts to use with AI assistants when implementing Storyteller SDK:</p> <ol> <li> <p>\"Using the Storyteller SDK documentation, help me initialize the SDK in my Swift iOS app.\"</p> </li> <li> <p>\"Based on the Storyteller documentation, what's the best way to implement a StorytellerStoriesRow SwiftUI view in my app?\"</p> </li> <li> <p>\"Help me troubleshoot this initialization error with Storyteller SDK: [paste your error]\"</p> </li> <li> <p>\"Using the Storyteller docs context, show me how to customize the theme of my StorytellerStoriesRow\"</p> </li> <li> <p>\"With the Storyteller documentation, help me implement analytics tracking for story views.\"</p> </li> </ol>"},{"location":"AI/#additional-resources","title":"Additional Resources","text":"<ul> <li>Storyteller Showcase App</li> </ul>"},{"location":"AdditionalMethods/","title":"Additional Methods","text":"<p>This page is a reference for <code>Storyteller.shared</code> properties and methods that are useful during integration.</p> <p>For dedicated topics, see:</p> <ul> <li>Opening content</li> <li>Deep linking</li> <li>Search</li> <li>Ads</li> <li>Analytics</li> </ul>"},{"location":"AdditionalMethods/#table-of-contents","title":"Table of Contents","text":"<ul> <li>Properties</li> <li>Methods</li> </ul>"},{"location":"AdditionalMethods/#properties","title":"Properties","text":""},{"location":"AdditionalMethods/#delegate","title":"<code>delegate</code>","text":"<p>The delegate is responsible for things like fetching ads and handling in-app navigation. More detailed info can be found in Storyteller Delegates.</p>"},{"location":"AdditionalMethods/#modules","title":"<code>modules</code>","text":"<p>This is a list of StorytellerModule instances that can extend Storyteller (for example: host-supplied ads), alongside the delegate.</p> <p>See the Showcase setup that registers modules and the delegate in <code>AppDelegate.setupStoryteller</code>.</p>"},{"location":"AdditionalMethods/#currentapikey","title":"<code>currentApiKey</code>","text":"<p>This is the current API key set when using <code>Storyteller.shared.initialize(...)</code>.</p>"},{"location":"AdditionalMethods/#version","title":"<code>version</code>","text":"<p>This is the current SDK version (for example <code>\"11.4.0\"</code>).</p> <pre><code>let version = Storyteller.shared.version\n</code></pre>"},{"location":"AdditionalMethods/#isinitialized","title":"<code>isInitialized</code>","text":"<p>This value becomes <code>true</code> after Storyteller is successfully initialized. Calling <code>initialize</code> again resets it to <code>false</code> until that call succeeds; it remains <code>false</code> if initialization throws.</p> <pre><code>let isInitialized = Storyteller.shared.isInitialized\n</code></pre>"},{"location":"AdditionalMethods/#ispresentingcontent","title":"<code>isPresentingContent</code>","text":"<p>A Boolean value indicating whether Storyteller content is currently presented.</p> <p>This property is <code>true</code> when a Story Player, Clip Player, Search or Sheet is visible, and <code>false</code> otherwise. It updates automatically when content is presented or dismissed.</p>"},{"location":"AdditionalMethods/#isplayermuted","title":"<code>isPlayerMuted</code>","text":"<p>This instance property indicates whether the current Storyteller Player is muted. It is isolated to the main actor, so read it from UI code or another main-actor context.</p> <pre><code>@MainActor\nfunc updateMuteIndicator() {\n    let isMuted = Storyteller.shared.isPlayerMuted\n    print(\"Player muted: \\(isMuted)\")\n}\n</code></pre>"},{"location":"AdditionalMethods/#issearchenabled","title":"<code>isSearchEnabled</code>","text":"<p>This value is <code>true</code> when the Search feature is enabled in the tenant configuration.</p>"},{"location":"AdditionalMethods/#theme","title":"<code>theme</code>","text":"<p>This is the default fallback theming style used to render Story or Clips items in lists and activities launched from lists.</p>"},{"location":"AdditionalMethods/#user","title":"<code>user</code>","text":"<p>Allows setting or removing custom attributes for personalization and audience targeting. See Working with Users.</p> <pre><code>Storyteller.shared.user.setCustomAttribute(key: \"location\", value: \"New York\")\n</code></pre>"},{"location":"AdditionalMethods/#eventtrackingoptions","title":"<code>eventTrackingOptions</code>","text":"<p>The <code>eventTrackingOptions</code> property customizes Storyteller's analytics and tracking behavior. This property is read-only and can only be set during SDK initialization. See Privacy and Tracking.</p>"},{"location":"AdditionalMethods/#methods","title":"Methods","text":"<p>Most of the methods below are async and should be called from a <code>Task</code> or another async context.</p>"},{"location":"AdditionalMethods/#initialize","title":"initialize","text":"<pre><code>try await Storyteller.shared.initialize(apiKey: \"YOUR_API_KEY\")\n</code></pre> <p>The <code>initialize</code> method is required to be called for Storyteller to work. It's recommended to fire this method as soon as possible in the app lifecycle.</p> <p>Parameters:</p> <ul> <li>apiKey - This is the API key provided by the Storyteller team.</li> <li>userInput - This is the ID of the user to be authorized. See Working with Users for more information on user IDs.</li> <li>eventTrackingOptions - Configures analytics and tracking behavior. Defaults to <code>.enableAll</code> which enables all tracking options. Once set during initialization, these options can only be changed by reinitializing the SDK. See Privacy and Tracking for more information.</li> </ul> <p>Throws - if there is an issue with initialization.</p>"},{"location":"AdditionalMethods/#dismissplayer","title":"dismissPlayer","text":"<pre><code>await Storyteller.shared.dismissPlayer(animated: true, dismissReason: \"host-navigation\")\n</code></pre> <p>The <code>dismissPlayer()</code> force closes the currently open Story or Clips Player. If no Story or Clips Player is open when this is called, it has no effect.</p> <p>Parameters:</p> <ul> <li><code>animated</code> - this decides whether to animate the dismissing of the view or not</li> <li><code>dismissReason</code> - an optional host-defined reason for closing the Story, Page, or Clip. When supplied, it populates <code>StorytellerUserActivityData.dismissedReason</code> on the corresponding dismissal event. Passing <code>nil</code> does not suppress the event; it leaves <code>dismissedReason</code> as <code>nil</code>. Delivery through <code>onUserActivityOccurred</code> is controlled by <code>enableUserActivityTracking</code> and, for Ad events, <code>enableAdTracking</code>.</li> </ul>"},{"location":"AdditionalMethods/#opensearch","title":"openSearch","text":"<pre><code>await Storyteller.shared.openSearch()\n</code></pre> <p>This method opens the Search screen when <code>isSearchEnabled</code> is <code>true</code>. If Search is disabled for the tenant, the method returns without presenting anything. See Search for a prerequisite check and complete invocation.</p>"},{"location":"AdditionalMethods/#resumeplayer","title":"resumePlayer","text":"<pre><code>Storyteller.shared.resumePlayer()\n</code></pre> <p>This method resumes the currently open Story or Player Views. If no Player is open when this is called, it has no effect.</p> <p>If you use <code>Storyteller.shared.useCustomShareHandling = true</code>, call <code>resumePlayer()</code> after dismissing your custom share UI so the paused Storyteller content can continue playback.</p>"},{"location":"AdditionalMethods/#opensheet","title":"openSheet","text":"<pre><code>try await Storyteller.shared.openSheet(id: \"sheet-id\")\n</code></pre> <p>This method loads and opens a Sheet with the corresponding ID.</p>"},{"location":"AdditionalMethods/#getstoriescount","title":"getStoriesCount","text":"<pre><code>let storiesCount = await Storyteller.shared.getStoriesCount(for: [\"category-id\"])\n</code></pre> <p>This method retrieves the total count of Stories for the specified category IDs.</p>"},{"location":"AdditionalMethods/#getclipscount","title":"getClipsCount","text":"<pre><code>let clipsCount = await Storyteller.shared.getClipsCount(for: \"collection-id\")\n</code></pre> <p>This method retrieves the total count of Clips in the specified Collection.</p>"},{"location":"Ads/","title":"Ads","text":""},{"location":"Ads/#introduction","title":"Introduction","text":"<p>The Storyteller SDK supports displaying ads that can be created in the Storyteller CMS (First Party Ads), as well as Ads from Google Ad Manager, Google AdMob, VAST tags via SDK extensions developed by Storyteller, and Ads from other sources via custom implementation provided by the integrator.</p> <p>Which source of ads is used can be configured on your behalf by a member of the Storyteller Delivery Team.</p> <p>If Ads do not appear, follow the shared Ads troubleshooting route first to identify whether the last successful boundary belongs to the tenant strategy, app extension, external provider, or SDK rendering.</p>"},{"location":"Ads/#storyteller-first-party-ads","title":"Storyteller First Party Ads","text":"<p>If your tenant is configured to use Storyteller First Party Ads, which can be managed in the Storyteller CMS, then no changes to the Storyteller integration code are necessary. The Ads code is managed entirely within the Storyteller SDK.</p>"},{"location":"Ads/#storyteller-gam-sdk","title":"Storyteller GAM SDK","text":"<p>To use Ads from Google Ad Manager in Storyteller, first reach out to your Storyteller contact and they will assist you with setting up Google Ad Manager to traffic ads to Storyteller.</p> <p>You will then need to use the Storyteller Google Ad Manager SDK extension to fetch the ads from Google Ad Manager.</p> <p>To use this extension, first install it using Swift Package Manager or Cocoapods.</p> <p>For Swift Package Manager, it is available on Github here:</p> <pre><code>https://github.com/getstoryteller/storyteller-gam-module-swift\n</code></pre> <p>For Cocoapods, first make sure to specify the sources for Cocoapods:</p> <pre><code>source 'https://github.com/getstoryteller/storyteller-sdk-ios-podspec.git'\nsource 'https://github.com/getstoryteller/storyteller-lottie-ios-podspec.git'\nsource 'https://cdn.cocoapods.org/'\n</code></pre> <p>The StorytellerGAMIntegration is available by importing this pod:</p> <pre><code>pod 'StorytellerGAMIntegration'\n</code></pre>"},{"location":"Ads/#basic-setup","title":"Basic Setup","text":"<p>Make sure to import the same version of <code>StorytellerSDK</code> and <code>StorytellerGAMIntegration</code>.</p> <p>Now initialize the extension as follows:</p> <pre><code>import StorytellerSDK\nimport StorytellerGAMIntegration\n\nlet configuration = StorytellerGAMModuleConfiguration(\n    adUnit: { requestInfo in\n        return \"YOUR_AD_UNIT_ID\"\n    }\n)\n</code></pre> <p>You will need to supply the following parameter:</p> Parameter Name Description <code>adUnit</code> A closure that returns the ID of the Ad unit in Google Ad Manager that will be used to serve the Storyteller Ads for the specific Ad request. This can be used for custom Ad units depending on the request context. <code>bottomBannerAdUnit</code> Optional closure that returns the Ad unit ID used specifically for the Clips bottom banner placement. Leave this <code>nil</code> if you don't plan to serve Clips bottom banner Ads. <p>Then pass the newly created instance of the extension to the <code>modules</code> property on the <code>Storyteller</code> instance:</p> <pre><code>let configuration = StorytellerGAMModuleConfiguration(\n    adUnit: { _ in \"YOUR_AD_UNIT_ID\" }\n)\nStoryteller.shared.modules = [StorytellerGAMModule(configuration: configuration)]\n</code></pre> <p>Our Showcase app uses this module to integrate ads - see the GAM module configuration in <code>AppDelegate.setupStoryteller</code>.</p>"},{"location":"Ads/#setup-with-dynamic-ad-unit-changes","title":"Setup with Dynamic Ad Unit Changes","text":"<p>Example for dynamic Ad unit changes when you want to use different Ad units for Stories and Clips:</p> <pre><code>import StorytellerSDK\nimport StorytellerGAMIntegration\n\nlet configuration = StorytellerGAMModuleConfiguration(\n    adUnit: { requestInfo in\n        switch requestInfo {\n        case .stories:\n            return \"/33813572/storyteller/stories\"\n        case .clips:\n            return \"/33813572/storyteller/clips\"\n        @unknown default:\n            return \"/33813572/storyteller/default\"\n        }\n    }\n)\n</code></pre>"},{"location":"Ads/#setup-with-additional-parameters","title":"Setup with Additional Parameters","text":"<p>You can also supply optional parameters <code>customNativeTemplateIds</code>, <code>publisherProvidedId</code>, <code>customKvps</code>, and <code>configureAdRequest</code> if needed:</p> <pre><code>import GoogleMobileAds\nimport StorytellerSDK\nimport StorytellerGAMIntegration\n\nlet configuration = StorytellerGAMModuleConfiguration(\n    adUnit: { requestInfo in\n        switch requestInfo {\n        case .stories:\n            return \"/33813572/storyteller/stories\"\n        case .clips:\n            return \"/33813572/storyteller/clips\"\n        @unknown default:\n            return \"/33813572/storyteller/default\"\n        }\n    },\n    customNativeTemplateIds: StorytellerGAMModuleConfiguration.CustomNativeTemplateIds(\n        stories: \"YOUR_STORIES_TEMPLATE_ID\",\n        clips: \"YOUR_CLIPS_TEMPLATE_ID\"\n    ),\n    publisherProvidedId: {\n        \"YOUR_PUBLISHER_PROVIDED_ID\"\n    },\n    customKvps: {\n        [\n            \"YOUR_CUSTOM_KEY\": \"YOUR_CUSTOM_VALUE\",\n            \"YOUR_PRIVACY_KEY\": \"YOUR_PRIVACY_VALUE\"\n        ]\n    },\n    configureAdRequest: { _, request in\n        var targeting = request.customTargeting ?? [:]\n        targeting[\"host_app_context\"] = \"sports\"\n        request.customTargeting = targeting\n    }\n)\n</code></pre> <p>This example adds a host-owned GAM targeting value. You can also use <code>configureAdRequest</code> to await your app's Sliide, APS, Nimbus, or other bidder adapter and apply its response to the request before Storyteller starts loading the Ad.</p> Parameter Name Description <code>customNativeTemplateIds</code> If you have worked with the Storyteller Delivery team to setup Custom Native Ads, you will need to supply their IDs here. If you are only using Stories (but not Clips) it is only necessary to supply one property of this struct. <code>publisherProvidedId</code> Optional closure that returns the Publisher Provided ID (PPID) for Google Ad Manager audience targeting. Return the identifier your GAM setup expects, or <code>nil</code> to omit PPID. <code>customKvps</code> A closure that is called each time we request a new ad. The Storyteller GAM SDK passes a default set of KVPs to GAM to allow targeting based on the content of the Stories/Clips the user is viewing. If you have any additional parameters that you need to be able to target by, these should be passed here. Note that the SDK will not inherit any KVPs being set in the rest of your app. Do not pass PPID here; use <code>publisherProvidedId</code> for PPID. <code>configureAdRequest</code> Optional async main-actor closure called after Storyteller applies its request setup, default KVPs, <code>customKvps</code>, and <code>publisherProvidedId</code>, but before it loads the Google request. Storyteller awaits this closure before calling the Google load API. Use it for host-owned bidder setup or Google request fields that cannot be expressed as KVPs or PPID. <p>Storyteller default KVPs and <code>customKvps</code> are only sent when ad tracking is enabled. <code>publisherProvidedId</code> and <code>configureAdRequest</code> are host-controlled request customizations; return <code>nil</code> for PPID and avoid mutating the request when your app should not send those values. <code>configureAdRequest</code> runs on the main actor for full-screen native, custom-template, and banner-fallback GAM requests, and for Clips bottom-banner GAM requests. It is not automatically privacy-gated by Storyteller. Apply any consent, limited-ad-tracking, or regional privacy checks required by your app before mutating the request. Because Storyteller awaits this callback before loading the Google request, keep bidder work bounded and handle any timeout or cancellation fallback inside your app. Storyteller-owned GAM KVP names beginning with <code>st</code> are reserved; do not overwrite or remove them in the callback. If <code>configureAdRequest</code> is omitted, Storyteller builds and loads the request using the existing request behavior.</p>"},{"location":"Ads/#default-kvps","title":"Default KVPs","text":"<p>The Storyteller GAM SDK automatically sends a set of key-value pairs (KVPs) to Google Ad Manager to enable content-based targeting. These KVPs are only sent when ad tracking is enabled (<code>enableAdTracking == true</code> in the <code>StorytellerEventTrackingOptions</code> you pass to <code>Storyteller.shared.initialize(...)</code>).</p> <p>For Stories:</p> KVP Key Description <code>stCategories</code> Categories associated with the current story <code>stCurrentCategory</code> Categories of the list containing the story <code>stPlacement</code> The placement identifier of the story <code>stApiKey</code> The current Storyteller API key <code>stAdIndex</code> The order of the ad within the story <p>For Clips:</p> KVP Key Description <code>stCollection</code> The identifier of the clip collection <code>stClipCategories</code> Categories associated with the current clip <code>stNextClipCategories</code> Categories of the next clip (if available) <code>stApiKey</code> The current Storyteller API key <code>stAdIndex</code> The order of the ad within the clip collection"},{"location":"Ads/#storyteller-vast-sdk","title":"Storyteller VAST SDK","text":"<p>The Storyteller VAST SDK extension requests vendor-neutral HTTPS VAST tags, resolves compatible inline and wrapper responses, selects playable linear media, and returns Storyteller-rendered fullscreen Ads through the standard module system.</p> <p>The module is generic VAST support, not an ITV-specific integration. ITV is the first validated production-shaped scenario for the iOS VAST module, but the same module can be configured for any compatible VAST server.</p> <p>To use this extension, first install it using Swift Package Manager or Cocoapods.</p> <p>For Swift Package Manager, it is available on Github here:</p> <pre><code>https://github.com/getstoryteller/storyteller-vast-module-swift\n</code></pre> <p>For Cocoapods, first make sure to specify the sources for Cocoapods:</p> <pre><code>source 'https://github.com/getstoryteller/storyteller-sdk-ios-podspec.git'\nsource 'https://github.com/getstoryteller/storyteller-lottie-ios-podspec.git'\nsource 'https://cdn.cocoapods.org/'\n</code></pre> <p>The StorytellerVASTIntegration is available by importing this pod:</p> <pre><code>pod 'StorytellerVASTIntegration'\n</code></pre>"},{"location":"Ads/#basic-setup_1","title":"Basic Setup","text":"<p>Make sure to import the same version of <code>StorytellerSDK</code> and <code>StorytellerVASTIntegration</code>.</p> <pre><code>import StorytellerSDK\nimport StorytellerVASTIntegration\n\nlet configuration = StorytellerVASTModuleConfiguration(\n    baseUrl: \"https://ads.example.com/vast\",\n    requestParameters: { _ in\n        [\n            \"placement\": \"storyteller\"\n        ]\n    },\n    urlFormat: .queryString\n)\n\nStoryteller.shared.modules = [StorytellerVASTModule(configuration: configuration)]\n</code></pre> <p>You will need to supply the following parameters:</p> Parameter Name Description <code>baseUrl</code> HTTPS base URL of the VAST tag endpoint. <code>requestParameters</code> Closure called for each Ad request. Return the key-value parameters your VAST provider needs for the specific Stories or Clips request context. <code>urlFormat</code> Optional serialization strategy. Use <code>.pathSegment</code> to append parameters as <code>/key=value</code> path segments, or <code>.queryString</code> to append them as query parameters. Defaults to <code>.pathSegment</code>. <code>diagnosticsHandler</code> Optional closure that receives request, parse, wrapper, media-selection, mapping, completion, and failure diagnostics. <p>The generated VAST tag URL must use HTTPS and be no longer than 2,048 bytes. The module supports compatible VAST 2.x, 3.x, and 4.x linear video responses, including wrappers up to the module depth limit. Unsupported creative types such as VPAID JavaScript are ignored during media selection.</p> <p>The VAST module supports fullscreen Story and Clip Ads. It does not serve Clips bottom banner Ads.</p>"},{"location":"Ads/#gam-vast-setup","title":"GAM VAST Setup","text":"<p>If you use Google Ad Manager to serve VAST video tags, use <code>StorytellerGAMVASTModule</code>. This module is part of <code>StorytellerVASTIntegration</code>; it does not use the Google Mobile Ads SDK or IMA SDK. It builds a GAM VAST tag request and then uses the same Storyteller VAST request, parsing, tracking, and fullscreen Player flow as <code>StorytellerVASTModule</code>.</p> <pre><code>import StorytellerSDK\nimport StorytellerVASTIntegration\n\nlet configuration = StorytellerGAMVASTModuleConfiguration(\n    adUnit: { requestInfo in\n        switch requestInfo {\n        case .stories:\n            return \"/33813572/storyteller/stories_vast\"\n        case .clips:\n            return \"/33813572/storyteller/clips_vast\"\n        @unknown default:\n            return \"/33813572/storyteller/default_vast\"\n        }\n    },\n    descriptionUrl: { _ in\n        \"https://example.com/storyteller-video\"\n    },\n    contentUrl: { _ in\n        \"https://example.com/storyteller-video\"\n    },\n    customParams: { _ in\n        [\n            \"sliide_content_category\": \"sports\"\n        ]\n    }\n)\n\nStoryteller.shared.modules = [StorytellerGAMVASTModule(configuration: configuration)]\n</code></pre> <p>You will need to supply the following parameters:</p> Parameter Name Description <code>adUnit</code> Required closure that returns the GAM Ad Unit path for the current Ad request. This becomes the GAM <code>iu</code> parameter. <code>descriptionUrl</code> Required closure that returns the canonical HTTPS URL describing the video content or Player context. This becomes the GAM <code>description_url</code> parameter. <code>contentUrl</code> Optional closure that returns the page or content URL to send as the GAM <code>url</code> parameter. Return <code>nil</code> to omit it. <code>customParams</code> Optional closure that returns GAM custom targeting parameters. The SDK serializes these into GAM's <code>cust_params</code> value, so pass unencoded keys and values. <code>tagParameters</code> Optional closure that returns extra top-level GAM VAST tag parameters. These are applied after the SDK-generated GAM parameters, so a matching key overrides the generated value. <code>diagnosticsHandler</code> Optional closure that receives the same VAST diagnostics events as <code>StorytellerVASTModule</code>. <p><code>StorytellerGAMVASTModule</code> generates a request to <code>https://pubads.g.doubleclick.net/gampad/ads</code> with query-string parameters. It supplies <code>iu</code>, <code>output=vast</code>, <code>env=vp</code>, <code>gdfp_req=1</code>, <code>sz</code>, <code>correlator</code>, <code>description_url</code>, optional <code>url</code>, <code>vpa=auto</code>, <code>vpmute</code>, optional <code>cust_params</code>, and the SDK-owned VAST bid parameters listed below. Use <code>tagParameters</code> for any additional GAM VAST tag parameters required by your ad server setup.</p> <p>The SDK serializes <code>sz</code> as <code>&lt;width&gt;x&lt;height&gt;</code> in physical pixels, with a lowercase <code>x</code> and no spaces or <code>px</code> suffix. It derives this from the active fullscreen Player window or screen and omits <code>sz</code> only when it cannot resolve a reliable non-zero size. <code>customParams</code> are serialized inside <code>cust_params</code>; for example, <code>[\"sliide_content_category\": \"sports\"]</code> becomes <code>sliide_content_category=sports</code> inside the <code>cust_params</code> value before the final request URL is encoded.</p> <p>GAM VAST is VAST-backed, not Google Mobile Ads or IMA-backed. The module sets <code>adSource</code> to <code>.custom(\"vast\")</code>, so Google paid ad analytics events are not emitted for GAM VAST requests.</p> <p>See VAST and GAM VAST Parameter Tables for the parameters the SDK sets internally, where client-provided values are applied, and which values can be overridden.</p> <p>The GAM VAST module supports fullscreen Story and Clip Ads. It does not serve Clips bottom banner Ads.</p>"},{"location":"Ads/#vast-and-gam-vast-parameter-tables","title":"VAST and GAM VAST Parameter Tables","text":"<p>Use these tables to decide whether a value belongs in generic VAST <code>requestParameters</code>, GAM VAST <code>customParams</code>, or GAM VAST <code>tagParameters</code>.</p> Surface Client Parameter Path SDK Behavior Override Behavior Generic VAST tag parameters <code>StorytellerVASTModuleConfiguration.requestParameters</code> The SDK builds the Sliide VAST bid parameters first, then merges the returned key-value pairs and serializes the final result using the configured <code>urlFormat</code>. It does not add GAM-specific parameters. Matching keys in <code>requestParameters</code> override SDK-set Sliide VAST bid parameters. GAM VAST custom targeting <code>StorytellerGAMVASTModuleConfiguration.customParams</code> The SDK encodes the returned key-value pairs into GAM's <code>cust_params</code> value. Change individual custom targeting values in <code>customParams</code>, or replace the generated <code>cust_params</code> by returning <code>cust_params</code> from <code>tagParameters</code>. GAM VAST top-level parameters <code>StorytellerGAMVASTModuleConfiguration.tagParameters</code> The SDK builds GAM VAST parameters and Sliide VAST bid parameters first, then applies these top-level parameters. Matching keys in <code>tagParameters</code> are the final override layer. Sliide VAST Bid Parameter Applies To Value / Source Static or Dynamic Client Override <code>adtype</code> Generic VAST and GAM VAST <code>13</code> Static Generic: <code>requestParameters[\"adtype\"]</code>; GAM VAST: <code>tagParameters[\"adtype\"]</code> <code>plcmt</code> Generic VAST and GAM VAST <code>3</code> Static Generic: <code>requestParameters[\"plcmt\"]</code>; GAM VAST: <code>tagParameters[\"plcmt\"]</code> <code>vw</code> Generic VAST and GAM VAST Player width in physical pixels, for example <code>1080</code> Dynamic; omitted when no reliable size is available Generic: <code>requestParameters[\"vw\"]</code>; GAM VAST: <code>tagParameters[\"vw\"]</code> <code>vh</code> Generic VAST and GAM VAST Player height in physical pixels, for example <code>1920</code> Dynamic; omitted when no reliable size is available Generic: <code>requestParameters[\"vh\"]</code>; GAM VAST: <code>tagParameters[\"vh\"]</code> <code>vminl</code> Generic VAST and GAM VAST <code>5</code> Static Generic: <code>requestParameters[\"vminl\"]</code>; GAM VAST: <code>tagParameters[\"vminl\"]</code> <code>vmaxl</code> Generic VAST and GAM VAST <code>30</code> Static Generic: <code>requestParameters[\"vmaxl\"]</code>; GAM VAST: <code>tagParameters[\"vmaxl\"]</code> <code>vfmt</code> Generic VAST and GAM VAST <code>1</code> for MP4 playable media format on iOS Static Generic: <code>requestParameters[\"vfmt\"]</code>; GAM VAST: <code>tagParameters[\"vfmt\"]</code> <code>vadFmt</code> Generic VAST and GAM VAST <code>2+3+8</code> Static Generic: <code>requestParameters[\"vadFmt\"]</code>; GAM VAST: <code>tagParameters[\"vadFmt\"]</code> <code>vplay</code> Generic VAST and GAM VAST <code>6</code> for muted autoplay, <code>5</code> for sound-on autoplay Dynamic; falls back to <code>6</code> when mute state is unavailable Generic: <code>requestParameters[\"vplay\"]</code>; GAM VAST: <code>tagParameters[\"vplay\"]</code> <code>vskip</code> Generic VAST and GAM VAST <code>1</code> Static Generic: <code>requestParameters[\"vskip\"]</code>; GAM VAST: <code>tagParameters[\"vskip\"]</code> <code>vpos</code> Generic VAST and GAM VAST <code>0</code> Static Generic: <code>requestParameters[\"vpos\"]</code>; GAM VAST: <code>tagParameters[\"vpos\"]</code> <code>vcom</code> Generic VAST and GAM VAST <code>0</code> Static Generic: <code>requestParameters[\"vcom\"]</code>; GAM VAST: <code>tagParameters[\"vcom\"]</code> <code>vcont</code> Generic VAST and GAM VAST <code>1</code> Static Generic: <code>requestParameters[\"vcont\"]</code>; GAM VAST: <code>tagParameters[\"vcont\"]</code> <code>vtype</code> Generic VAST and GAM VAST <code>1</code> Static Generic: <code>requestParameters[\"vtype\"]</code>; GAM VAST: <code>tagParameters[\"vtype\"]</code> <code>vminbtr</code> Generic VAST and GAM VAST <code>600</code> Static Generic: <code>requestParameters[\"vminbtr\"]</code>; GAM VAST: <code>tagParameters[\"vminbtr\"]</code> <code>vmaxbtr</code> Generic VAST and GAM VAST <code>8000</code> Static Generic: <code>requestParameters[\"vmaxbtr\"]</code>; GAM VAST: <code>tagParameters[\"vmaxbtr\"]</code> <p><code>vw</code> and <code>vh</code> are separate integer values in physical pixels. Do not include <code>px</code>, point units, spaces, or an <code>x</code> separator in those values. For a fullscreen player measured as 1080 by 1920 physical pixels, the SDK sends <code>vw=1080</code> and <code>vh=1920</code>.</p> <p>Pure generic VAST does not use GAM <code>cust_params</code>. If you need GAM custom targeting such as a content category, pass it through GAM VAST <code>customParams</code> so the SDK encodes it into <code>cust_params</code>.</p> GAM VAST Top-Level Parameter Set By Default Default Source Value Type Client Override <code>output</code> Yes SDK sets <code>vast</code>. Static <code>tagParameters[\"output\"]</code> <code>env</code> Yes SDK sets <code>vp</code>. Static <code>tagParameters[\"env\"]</code> <code>gdfp_req</code> Yes SDK sets <code>1</code>. Static <code>tagParameters[\"gdfp_req\"]</code> <code>iu</code> Yes <code>adUnit</code> closure. Dynamic per Ad request <code>tagParameters[\"iu\"]</code> <code>sz</code> When a reliable non-zero size is available. SDK derives active fullscreen Player window or screen physical pixels as <code>&lt;width&gt;x&lt;height&gt;</code>. Dynamic per request context <code>tagParameters[\"sz\"]</code> <code>correlator</code> Yes SDK generates a fresh value for each request. Dynamic per Ad request <code>tagParameters[\"correlator\"]</code> <code>description_url</code> Yes <code>descriptionUrl</code> closure. Dynamic per Ad request <code>tagParameters[\"description_url\"]</code> <code>url</code> When non-<code>nil</code> <code>contentUrl</code> closure. Dynamic per Ad request <code>tagParameters[\"url\"]</code> <code>vpa</code> Yes SDK sets <code>auto</code>. Static <code>tagParameters[\"vpa\"]</code> <code>vpmute</code> Yes SDK sets <code>1</code> when the Player is muted and <code>0</code> when unmuted. Dynamic per request context <code>tagParameters[\"vpmute\"]</code> <code>cust_params</code> When <code>customParams</code> returns at least one key-value pair. Encoded <code>customParams</code> output. Dynamic per Ad request <code>tagParameters[\"cust_params\"]</code> GAM VAST Custom Parameter Recommended Client Path Encoded Location Notes <code>sliide_content_category</code> <code>customParams[\"sliide_content_category\"]</code> Inside <code>cust_params</code>. Use this for Sliide content category targeting. Additional custom KVPs <code>customParams</code> Inside <code>cust_params</code>. Pass unencoded keys and values; the SDK handles GAM <code>cust_params</code> encoding."},{"location":"Ads/#vast-url-formats","title":"VAST URL Formats","text":"<p>Use <code>.pathSegment</code> when your VAST server expects parameters appended as path segments:</p> <pre><code>let configuration = StorytellerVASTModuleConfiguration(\n    baseUrl: \"https://ads.example.com/vast\",\n    requestParameters: { _ in\n        [\n            \"placement\": \"stories\",\n            \"adIndex\": \"1\"\n        ]\n    },\n    urlFormat: .pathSegment\n)\n</code></pre> <p>This produces a request shaped like:</p> <pre><code>https://ads.example.com/vast/placement=stories/adIndex=1\n</code></pre> <p>Use <code>.queryString</code> when your VAST server expects standard query parameters:</p> <pre><code>let configuration = StorytellerVASTModuleConfiguration(\n    baseUrl: \"https://ads.example.com/vast\",\n    requestParameters: { _ in\n        [\n            \"placement\": \"stories\",\n            \"adIndex\": \"1\"\n        ]\n    },\n    urlFormat: .queryString\n)\n</code></pre> <p>This produces a request shaped like:</p> <pre><code>https://ads.example.com/vast?placement=stories&amp;adIndex=1\n</code></pre> <p>The SDK encodes parameter keys and values for the selected format. Pass unencoded values from your callback.</p>"},{"location":"Ads/#vast-support-in-the-first-release","title":"VAST Support in the First Release","text":"<p>The first iOS release supports:</p> <ul> <li>Inline linear fullscreen video Ads for Stories and Clips</li> <li>Compatible VAST 2.x, 3.x, and 4.x parsing</li> <li>Wrapper resolution and fallback to the first playable Ad in the response</li> <li>Media file selection for compatible video media</li> <li>Click-through and click tracking</li> <li>Impression, creative view, start, quartile, complete, pause, resume, mute, unmute, close, and skip tracking</li> <li>Multiple tracking URLs for the same VAST event</li> <li>Deferred VAST error reporting when request, parse, wrapper, media selection, or mapping fails</li> <li>Storyteller presentation metadata through <code>Extension type=\"storyteller:ad-ui\"</code></li> <li>One compatible static-resource VAST <code>&lt;Icon&gt;</code> overlay for fullscreen Story and Clip Ads</li> </ul> <p>The first iOS release does not support:</p> <ul> <li>Nonlinear Ads</li> <li>Companion Ads</li> <li>OMID / verification rendering</li> <li>VPAID or SIMID</li> <li>Server-side ad insertion (SSAI)</li> <li>Clips bottom banner Ads through VAST</li> <li>VAST ad caption rendering</li> <li>Rendering VAST <code>&lt;IFrameResource&gt;</code> or <code>&lt;HTMLResource&gt;</code> icons</li> </ul>"},{"location":"Ads/#vast-tracking-and-skip-behavior","title":"VAST Tracking and Skip Behavior","text":"<p>When a VAST response contains multiple URLs for the same supported tracking event, the SDK preserves and fires all of them for the matching Storyteller playback event.</p> <p>VAST <code>skip</code> tracking maps to the Storyteller skipped-Ad flow. VAST <code>skipoffset</code> is converted to an ad-specific non-skippable duration in the Storyteller Player. When <code>skipoffset</code> is present, it controls the countdown for that VAST Ad; when it is absent, the SDK uses the tenant's CMS-configured non-skippable Ads behavior.</p> <p>VAST <code>&lt;Error&gt;</code> URLs are reported by the VAST module when the SDK cannot request, parse, resolve, select media for, or map a VAST Ad. These failures cause the module to fail safely so Storyteller can continue through the normal module fallback flow.</p>"},{"location":"Ads/#vast-branded-presentation-metadata","title":"VAST Branded Presentation Metadata","text":"<p>VAST ads can provide Storyteller presentation metadata through <code>Extension type=\"storyteller:ad-ui\"</code>. The SDK reads <code>CtaText</code> and <code>AdvertiserName</code> from the <code>StorytellerAdUi</code> child element:</p> <pre><code>&lt;Extension type=\"storyteller:ad-ui\"&gt;\n  &lt;StorytellerAdUi&gt;\n    &lt;CtaText&gt;Shop now&lt;/CtaText&gt;\n    &lt;AdvertiserName&gt;Example Brand&lt;/AdvertiserName&gt;\n  &lt;/StorytellerAdUi&gt;\n&lt;/Extension&gt;\n</code></pre> <p>The SDK resolves the final presentation values with this priority:</p> Field Resolution priority CTA text VAST extension <code>CtaText</code> -&gt; backend default <code>ads.presentationDefaults.vast.ctaText</code> -&gt; <code>Learn more</code> Advertiser display name VAST extension <code>AdvertiserName</code> -&gt; backend default <code>ads.presentationDefaults.vast.advertiserName</code> -&gt; VAST <code>&lt;Advertiser&gt;</code> -&gt; VAST <code>&lt;AdTitle&gt;</code> <p>CTA text is only shown when the VAST ad has a non-empty <code>&lt;ClickThrough&gt;</code> destination. VAST <code>&lt;Icon&gt;</code> elements are reserved for industry, privacy, or program overlays and are not used as brand logos.</p>"},{"location":"Ads/#vast-icon-overlays","title":"VAST Icon Overlays","text":"<p>For fullscreen Story and Clip VAST ads, the SDK renders one compatible static-resource <code>&lt;Icon&gt;</code> overlay per ad only when the icon has <code>program=\"AdChoices\"</code>. Non-AdChoices icons, <code>&lt;IFrameResource&gt;</code> icons, and <code>&lt;HTMLResource&gt;</code> icons are parsed for diagnostics but are not rendered.</p> <p>The icon is rendered inside the video frame, including landscape videos that are aspect-fitted inside the Player. The SDK honors VAST <code>xPosition</code> values of <code>left</code>, <code>right</code>, or a numeric x-coordinate, and <code>yPosition</code> values of <code>top</code>, <code>bottom</code>, or a numeric y-coordinate. The overlay has a fixed 24pt height and preserves the static resource aspect ratio for its width. VAST <code>offset</code> and <code>duration</code> values control when the icon appears, <code>IconViewTracking</code> is fired once when it first becomes visible, and <code>IconClickTracking</code> is fired when the icon is tapped. If <code>IconClickThrough</code> is present, tapping the icon opens that destination externally.</p>"},{"location":"Ads/#setup-with-dynamic-parameters","title":"Setup with Dynamic Parameters","text":"<p>Use <code>StorytellerAdRequestInfo</code> to provide different parameters for Stories and Clips:</p> <pre><code>let configuration = StorytellerVASTModuleConfiguration(\n    baseUrl: \"https://ads.example.com/vast\",\n    requestParameters: { requestInfo in\n        switch requestInfo {\n        case let .stories(placement, categories, story, adIndex):\n            return [\n                \"placement\": placement,\n                \"categories\": categories.joined(separator: \",\"),\n                \"storyCategories\": story.categories.map(\\.externalId).joined(separator: \",\"),\n                \"adIndex\": \"\\(adIndex)\"\n            ]\n        case let .clips(collection, clip, nextClip, adIndex):\n            return [\n                \"collection\": collection,\n                \"clipCategories\": clip.categories.map(\\.externalId).joined(separator: \",\"),\n                \"nextClipCategories\": nextClip?.categories.map(\\.externalId).joined(separator: \",\") ?? \"\",\n                \"adIndex\": \"\\(adIndex)\"\n            ]\n        @unknown default:\n            return [:]\n        }\n    },\n    urlFormat: .queryString\n)\n</code></pre>"},{"location":"Ads/#storyteller-admob-sdk","title":"Storyteller AdMob SDK","text":"<p>AdMob support uses the same StorytellerGAMIntegration artifact but a different module entry point. Only one ads integration module can be used at a time.</p> <p>Due to AdMob limitations, banner Ads cannot be served from the same Ad unit as native Ads. <code>adUnit</code> is always used for native Ads, while <code>bannerAdUnit</code> is used for fullscreen banner Ads.</p> <ul> <li>Default (<code>enableBannerAdPriority = false</code>): the module tries to load a native Ad from <code>adUnit</code> first. If native loading fails and <code>bannerAdUnit</code> is configured, it falls back to a fullscreen banner Ad from <code>bannerAdUnit</code>.</li> <li>Banner priority enabled (<code>enableBannerAdPriority = true</code>): if <code>bannerAdUnit</code> is configured, the module tries to load a fullscreen banner Ad first. If banner loading fails, it falls back to a native Ad from <code>adUnit</code>.</li> </ul> <p><code>enableBannerAdPriority</code> only affects this fullscreen fallback order. It has no effect when <code>bannerAdUnit</code> is <code>nil</code>, and it does not change Clips bottom banner behavior configured through <code>bottomBannerAdUnit</code>.</p>"},{"location":"Ads/#basic-setup_2","title":"Basic Setup","text":"<pre><code>import StorytellerSDK\nimport StorytellerGAMIntegration\n\nlet configuration = StorytellerAdMobModuleConfiguration(\n    adUnit: { requestInfo in\n        return \"YOUR_NATIVE_AD_UNIT_ID\"\n    },\n    bannerAdUnit: { requestInfo in\n        return \"YOUR_BANNER_AD_UNIT_ID\"\n    },\n    bottomBannerAdUnit: { requestInfo in\n        return \"YOUR_BOTTOM_BANNER_AD_UNIT_ID\"\n    }\n)\n\nStoryteller.shared.modules = [StorytellerAdMobModule(configuration: configuration)]\n</code></pre> Parameter Name Description <code>adUnit</code> Required closure that returns the native Ad unit ID. <code>bannerAdUnit</code> Optional closure that returns a fullscreen banner Ad unit ID. If supplied, the module can use it as the banner fallback path or the banner-first path when <code>enableBannerAdPriority</code> is enabled. <code>bottomBannerAdUnit</code> Optional closure that returns the Ad unit ID used specifically for the Clips bottom banner placement. Leave this <code>nil</code> if you don't plan to serve Clips bottom banner Ads. <code>customKvps</code> Optional closure that returns custom key-value pairs to attach to AdMob requests for targeting. <code>enableBannerAdPriority</code> Optional flag that changes the fullscreen Ad loading order to banner first, then native fallback. This flag only has an effect when <code>bannerAdUnit</code> is configured. <p>For a complete integration example, see our Showcase app code here.</p>"},{"location":"Ads/#setup-with-banner-priority-enabled","title":"Setup with Banner Priority Enabled","text":"<pre><code>import StorytellerSDK\nimport StorytellerGAMIntegration\n\nlet configuration = StorytellerAdMobModuleConfiguration(\n    adUnit: { _ in\n        \"YOUR_NATIVE_AD_UNIT_ID\"\n    },\n    bannerAdUnit: { _ in\n        \"YOUR_BANNER_AD_UNIT_ID\"\n    },\n    enableBannerAdPriority: true\n)\n\nStoryteller.shared.modules = [StorytellerAdMobModule(configuration: configuration)]\n</code></pre>"},{"location":"Ads/#setup-with-dynamic-ad-unit-changes_1","title":"Setup with Dynamic Ad Unit Changes","text":"<pre><code>import StorytellerSDK\nimport StorytellerGAMIntegration\n\nlet configuration = StorytellerAdMobModuleConfiguration(\n    adUnit: { requestInfo in\n        switch requestInfo {\n        case .stories:\n            return \"YOUR_STORIES_NATIVE_AD_UNIT_ID\"\n        case .clips:\n            return \"YOUR_CLIPS_NATIVE_AD_UNIT_ID\"\n        @unknown default:\n            return \"YOUR_DEFAULT_NATIVE_AD_UNIT_ID\"\n        }\n    },\n    bannerAdUnit: { requestInfo in\n        switch requestInfo {\n        case .stories:\n            return \"YOUR_STORIES_BANNER_AD_UNIT_ID\"\n        case .clips:\n            return \"YOUR_CLIPS_BANNER_AD_UNIT_ID\"\n        @unknown default:\n            return \"YOUR_DEFAULT_BANNER_AD_UNIT_ID\"\n        }\n    },\n    bottomBannerAdUnit: { requestInfo in\n        switch requestInfo {\n        case .stories:\n            return \"YOUR_STORIES_BOTTOM_BANNER_AD_UNIT_ID\"\n        case .clips:\n            return \"YOUR_CLIPS_BOTTOM_BANNER_AD_UNIT_ID\"\n        @unknown default:\n            return \"YOUR_DEFAULT_BOTTOM_BANNER_AD_UNIT_ID\"\n        }\n    },\n    customKvps: {\n        [\"appmode\": \"prod\"]\n    },\n    enableBannerAdPriority: true\n)\n</code></pre>"},{"location":"Ads/#admob-test-ids-for-local-validation","title":"AdMob Test IDs for Local Validation","text":"<p>The Showcase app uses the following sample AdMob unit IDs for local validation. Use your own production IDs outside test and debug flows.</p> Placement Sample AdMob unit ID Native <code>ca-app-pub-3940256099942544/3986624511</code> Native video <code>ca-app-pub-3940256099942544/2521693316</code> Fullscreen banner fallback <code>ca-app-pub-3940256099942544/2435281174</code> Clips bottom banner <code>ca-app-pub-3940256099942544/2934735716</code>"},{"location":"Ads/#admob-vs-gam-at-a-glance","title":"AdMob vs GAM at a Glance","text":"Concern GAM AdMob Module entry point <code>StorytellerGAMModule</code> <code>StorytellerAdMobModule</code> Native-specific options Supports <code>customNativeTemplateIds</code> for custom native Ads Uses standard native Ads and does not expose <code>customNativeTemplateIds</code> Fullscreen banner setup No separate <code>bannerAdUnit</code> parameter Uses optional <code>bannerAdUnit</code>; this can be banner fallback or banner-first when <code>enableBannerAdPriority</code> is <code>true</code> Shared options Supports <code>bottomBannerAdUnit</code>, <code>publisherProvidedId</code>, <code>customKvps</code>, and <code>configureAdRequest</code> Supports <code>bottomBannerAdUnit</code>, <code>customKvps</code>, and <code>enableBannerAdPriority</code>"},{"location":"Ads/#mutual-exclusivity","title":"Mutual Exclusivity","text":"<p><code>StorytellerGAMModule</code> and <code>StorytellerAdMobModule</code> are mutually exclusive. Configure only one of them at a time.</p> <p><code>StorytellerVASTModule</code> and <code>StorytellerGAMVASTModule</code> are separate fullscreen Ads modules. If you use either with any other Ads module, order the <code>Storyteller.shared.modules</code> array deliberately because the SDK asks modules for Ads in order and falls back to the next module when one throws.</p>"},{"location":"Ads/#bottom-banner-ads","title":"Bottom Banner Ads","text":"<p>The Clips Player supports bottom banner Ads rendered as standard banner views added to the hierarchy below the video view. Bottom banner Ads are disabled when <code>StorytellerClipCollectionConfiguration.adConfiguration</code> is omitted or set to <code>nil</code>, and for new <code>StorytellerClipsAdConfiguration</code> instances. To opt a Clips presentation into bottom banner Ads, pass <code>StorytellerClipsAdConfiguration(bottomBannerEnabled: true)</code> through <code>StorytellerClipCollectionConfiguration.adConfiguration</code>.</p> <p>When using the GAM or AdMob module, supply <code>bottomBannerAdUnit</code> in the corresponding configuration to fetch bottom banner Ads. Opting in locally does not force Ads on: the tenant feed response must still enable the placement with <code>showBottomBannerAd == true</code>, and the active Ads module must support it.</p> <p>Storyteller First Party Ads, <code>StorytellerVASTModule</code>, and <code>StorytellerGAMVASTModule</code> do not support bottom banner Ads.</p>"},{"location":"Ads/#clips-opening-pre-roll-ads","title":"Clips Opening Pre-Roll Ads","text":"<p>If your tenant is configured for Clips Ads with <code>initialIndex = 0</code>, individual Clips presentations can opt into an opening fullscreen Ad before the first Clip is played by passing <code>StorytellerClipsAdConfiguration(preRollEnabled: true)</code> through <code>StorytellerClipCollectionConfiguration.adConfiguration</code>.</p> <pre><code>let configuration = StorytellerClipCollectionConfiguration(\n    collectionId: \"top-plays\",\n    adConfiguration: StorytellerClipsAdConfiguration(preRollEnabled: true)\n)\n</code></pre> <p>Opening pre-roll is disabled when <code>StorytellerClipCollectionConfiguration.adConfiguration</code> is omitted or set to <code>nil</code>, and for new <code>StorytellerClipsAdConfiguration</code> instances. Opting in locally does not force Ads on: Clips Ads must still be available for the tenant, and the remote Clips Ad strategy must use <code>initialIndex = 0</code>. The request uses the first opened content Clip as the current Clip context and sends <code>adIndex = 1</code>, which maps to the default <code>stAdIndex = 1</code> value for Google Ad Manager and AdMob integrations.</p> <p>When a CMS-configured opening pre-roll timeout is reached before the Ad loads, the Player starts the content Clip and ignores any late opening pre-roll result. Later between-Clip Ads continue to use the normal Clips ad cadence and increment from the next Ad index.</p>"},{"location":"Ads/#ad-request-information","title":"Ad Request Information","text":"<p>Note: This section is only relevant if you're implementing a custom Ads solution or building VAST request parameters. If you're using Storyteller First Party Ads, <code>StorytellerGAMModule</code>, or <code>StorytellerAdMobModule</code>, you don't need to work with this directly.</p> <p>If your <code>StorytellerModule</code> (or <code>StorytellerDelegate</code>) provides integrating-app ads, set <code>adSource</code> to declare the source used by your implementation.</p> <ul> <li>For custom ad implementations, use <code>.custom(\"myNetwork\")</code>.</li> <li>For Google modules, use <code>.gam</code> for GAM and <code>.admob</code> for AdMob.</li> <li>The VAST and GAM VAST modules set <code>.custom(\"vast\")</code> automatically.</li> <li><code>.storyteller</code> is reserved for Storyteller First Party ads.</li> </ul> <p>Setting <code>adSource</code> to <code>.gam</code> or <code>.admob</code> enables Google paid ad analytics events. See Ad Events.</p> <p><code>StorytellerGAMModule</code>, <code>StorytellerAdMobModule</code>, <code>StorytellerVASTModule</code>, and <code>StorytellerGAMVASTModule</code> set <code>adSource</code> automatically.</p> <p>When implementing custom Ads, you'll receive context about the Ad request through the <code>StorytellerAdRequestInfo</code> enum. This provides information about what content the Ad will be displayed for.</p>"},{"location":"Ads/#storytelleradrequestinfo","title":"StorytellerAdRequestInfo","text":"<p>The <code>StorytellerAdRequestInfo</code> enum has two cases:</p> <ul> <li> <p><code>stories(placement: String, categories: [String], story: ItemInfo, adIndex: Int)</code></p> <p>Used when an Ad is requested for display in a Stories Player. The parameters include:</p> </li> <li> <p><code>placement</code> - The placement identifier of the Story</p> </li> <li><code>categories</code> - An array of categories associated with the List that the Story is part of</li> <li><code>story</code> - An <code>ItemInfo</code> struct containing more information about the specific Story</li> <li> <p><code>adIndex</code> - The order of the ad within the current playback session (starts from 1)</p> </li> <li> <p><code>clips(collection: String, clip: ItemInfo, nextClip: ItemInfo?, adIndex: Int)</code></p> <p>Used when an Ad is requested for display in a Clips Player. The parameters include:</p> </li> <li> <p><code>collection</code> - The identifier of the Clip collection</p> </li> <li><code>clip</code> - An <code>ItemInfo</code> struct containing detailed information about the current Clip</li> <li><code>nextClip</code> - An optional <code>ItemInfo</code> struct containing more information about the Clip that is to appear after the requested Ad</li> <li><code>adIndex</code> - The order of the Ad within the displayed Ads in a Clip collection (1 for the first Ad, 2 for the second, etc.)</li> </ul>"},{"location":"Ads/#iteminfo","title":"ItemInfo","text":"<p>Each case includes an <code>ItemInfo</code> struct that contains:</p> <ul> <li><code>categories</code> - An array of <code>StorytellerCategory</code> objects representing categories that the Story or Clip is part of.</li> </ul>"},{"location":"Ads/#non-skippable-ads","title":"Non Skippable Ads","text":"<p>Our Player can enforce a period of time during which ads can't be skipped. When enabled, user interactions that would skip a Story or Clip Ad won't be allowed for that duration. This feature can be configured in the CMS.</p>"},{"location":"Analytics/","title":"Analytics","text":""},{"location":"Analytics/#table-of-contents","title":"Table of Contents","text":"<ol> <li>Event Types</li> <li>Story Events</li> <li>Clip Events</li> <li>List Tile Events</li> <li>Card Events</li> <li>Ad Events</li> <li>Playback Events</li> <li>Sheet Events</li> <li>Search Events</li> <li>Event Data</li> </ol>"},{"location":"Analytics/#event-types","title":"Event Types","text":"<p>These are the various events which are triggered from within the SDK. Each event is a member of the <code>StorytellerUserActivity.EventType</code> enumeration. Follow Integrate Analytics to retain the delegate, choose tracking options, forward events, and verify the integration.</p> <p>For a concrete analytics integration, see the Showcase forwarding implementation in <code>StorytellerTrackingDelegate</code>.</p> <p>In the below discussion, \"completing\" a Page refers to allowing the timer to expire - so this would correspond to watching all of an Image Page for the duration set for it (default 15s) or watching all of a video.</p>"},{"location":"Analytics/#story-events","title":"Story Events","text":"<p>The following properties are included in Story-related events:</p> <ul> <li><code>airplayEnabled</code></li> <li><code>captionsEnabled</code></li> <li><code>contentLength</code></li> <li><code>currentCategory</code></li> <li><code>eyebrow</code></li> <li><code>isMuted</code></li> <li><code>metadata</code></li> <li><code>pageActionText</code></li> <li><code>pageActionType</code></li> <li><code>pageActionUrl</code></li> <li><code>actionLinkId</code></li> <li><code>pageHasAction</code></li> <li><code>pageId</code></li> <li><code>pageIndex</code></li> <li><code>pageTitle</code></li> <li><code>pageType</code></li> <li><code>storyDisplayTitle</code></li> <li><code>storyId</code></li> <li><code>storyPageCount</code></li> <li><code>storyPlaybackMode</code></li> <li><code>storyTitle</code></li> <li><code>context</code></li> </ul>"},{"location":"Analytics/#opened-story","title":"Opened Story","text":"<p>This event is recorded in the following scenarios:</p> <ul> <li>When a user taps on a row item to open a story</li> <li>When a story is loaded because the previous Story finished</li> <li>When a story is loaded because the user tapped to skip the last page of the previous story</li> <li>When a user swipes left on a story to go to the next story</li> <li>When a user swipes right on a story to go to the previous story</li> <li>When a user is sent directly to a story via a deep link</li> </ul> <p>Whenever an Opened Story event occurs, additional event data specific to this event includes <code>storyReadStatus</code>, <code>categories</code>, <code>categoryDetails</code>, <code>storyIndex</code>, and <code>openedReason</code>.</p>"},{"location":"Analytics/#opened-page","title":"Opened Page","text":"<p>This event is recorded whenever a user views content from a page.</p> <p>Opened Page is one of the most important events to track as it is equal to a video start, one of the most important measures of engagement. By tracking this event, you can monitor valuable information about user engagement with your app.</p> <p>Whenever an Opened Page event occurs, additional event data specific to this event includes <code>openedReason</code>.</p>"},{"location":"Analytics/#dismissed-story","title":"Dismissed Story","text":"<p>This event is recorded in the following scenarios:</p> <ul> <li>When a user taps the close button to dismiss the story</li> <li>When a user swipes down to dismiss the story</li> <li>When a user taps to skip the last page of the final story - this dismisses the story and exits the story view</li> <li>When a user swipes left on the final story to dismiss the story</li> <li>When a user swipes right on the first story to dismiss the story</li> <li>When a user completes the final page of the final story and the story view is dismissed</li> </ul> <p>Whenever a Dismissed Story event occurs, additional event data specific to this event includes <code>dismissedReason</code>, <code>durationViewed</code>, and <code>pagesViewedCount</code>.</p>"},{"location":"Analytics/#skipped-story","title":"Skipped Story","text":"<p>This event is recorded when a user swipes left to go to the next story (not on the last page).</p> <p>Note: This event is not fired when the user swipes on the last page of a story. In that case, the user has consumed all pages and is naturally moving to the next story or dismissing the player, which is not considered a \"skip\".</p>"},{"location":"Analytics/#skipped-page","title":"Skipped Page","text":"<p>This event is recorded when a user taps to go to the next page before completing the current page.</p>"},{"location":"Analytics/#completed-story","title":"Completed Story","text":"<p>This event is recorded at the same time as <code>OpenedPage</code> for the final page in a story.</p>"},{"location":"Analytics/#completed-page","title":"Completed Page","text":"<p>This event is recorded when a user watches a page to completion (i.e. the timer for that page finishes.)</p>"},{"location":"Analytics/#action-button-tapped","title":"Action Button Tapped","text":"<p>This event is recorded when a user taps an action button on a page to open a link.</p> <p>Whenever an Action Button Tapped event occurs, additional event data specific to this event includes <code>actionText</code>.</p>"},{"location":"Analytics/#share-button-tapped","title":"Share Button Tapped","text":"<p>This event is recorded when a user taps the share button on a page.</p> <p>Whenever a Share Button Tapped event occurs, additional event data specific to this event includes <code>shareMethod</code>.</p>"},{"location":"Analytics/#previous-story","title":"Previous Story","text":"<p>This event is recorded when:</p> <ul> <li>a user swipes right to go to the previous story (unless this is the first story - in which case <code>DismissedStory</code> is fired instead)</li> <li>a user taps back on the first page in a story (and this is not the first page in the first story)</li> </ul>"},{"location":"Analytics/#previous-page","title":"Previous Page","text":"<p>This event is recorded when a user taps back to go to a previous page in the story.</p>"},{"location":"Analytics/#share-success","title":"Share Success","text":"<p>This event is recorded when a user selects a sharing method from the system dialog.</p> <p>Whenever a Share Success event occurs, additional event data specific to this event includes <code>shareMethod</code>.</p>"},{"location":"Analytics/#voted-poll","title":"Voted Poll","text":"<p>This event is recorded when user votes in a poll.</p> <p>Whenever a Voted Poll event occurs, additional event data specific to this event includes <code>pollAnswerId</code>.</p>"},{"location":"Analytics/#trivia-quiz-question-answered","title":"Trivia Quiz Question Answered","text":"<p>This event is recorded when a user answers a question in a trivia quiz.</p> <p>Whenever a Trivia Quiz Question Answered event occurs, additional event data specific to this event includes <code>triviaQuizId</code>, <code>triviaQuizTitle</code>, <code>triviaQuizQuestionId</code>, and <code>triviaQuizAnswerId</code>.</p>"},{"location":"Analytics/#trivia-quiz-completed","title":"Trivia Quiz Completed","text":"<p>This event is recorded when a user completes a trivia quiz.</p> <p>Whenever a Trivia Quiz Completed event occurs, additional event data specific to this event includes <code>triviaQuizId</code>, <code>triviaQuizTitle</code>, and <code>triviaQuizScore</code>.</p>"},{"location":"Analytics/#story-instructions-screen-viewed","title":"Story Instructions Screen Viewed","text":"<p>This event is recorded when:</p> <ul> <li>The Story instruction screen appears to users</li> </ul> <p>Whenever a Story Instructions Screen Viewed event occurs, additional event data specific to this event includes <code>storyReadStatus</code> and <code>openedReason</code>.</p>"},{"location":"Analytics/#muted-story","title":"Muted Story","text":"<p>This event is recorded when:</p> <ul> <li>A user mutes a Story by toggling the mute button, adjusting device volume controls, or using the UI mute button</li> </ul>"},{"location":"Analytics/#unmuted-story","title":"Unmuted Story","text":"<p>This event is recorded when:</p> <ul> <li>A user unmutes a Story by toggling the mute button, adjusting device volume controls, or using the UI mute button</li> </ul>"},{"location":"Analytics/#clip-events","title":"Clip Events","text":"<p>The following properties are included in all Clip-related events:</p> <ul> <li><code>airplayEnabled</code></li> <li><code>captionsEnabled</code></li> <li><code>categories</code></li> <li><code>categoryDetails</code></li> <li><code>clipActionText</code></li> <li><code>clipActionType</code></li> <li><code>clipActionUrl</code></li> <li><code>actionLinkId</code></li> <li><code>clipCollectionCount</code></li> <li><code>clipFeedType</code></li> <li><code>clipHasAction</code></li> <li><code>clipHasSecondaryActions</code></li> <li><code>clipId</code></li> <li><code>clipIndex</code></li> <li><code>clipSecondaryActionTypes</code></li> <li><code>clipSecondaryActionUrls</code></li> <li><code>clipSecondaryActionsText</code></li> <li><code>clipTitle</code></li> <li><code>contentLength</code></li> <li><code>isLive</code></li> <li><code>isMuted</code></li> <li><code>metadata</code></li> </ul> <p>The following additional properties may be included when available:</p> <ul> <li><code>eyebrow</code></li> <li><code>collection</code></li> <li><code>collectionTitle</code></li> <li><code>categoryId</code></li> <li><code>categoryName</code></li> <li><code>context</code></li> </ul>"},{"location":"Analytics/#opened-clip","title":"Opened Clip","text":"<p>This event is recorded when:</p> <ul> <li>a user taps on a row or grid item to open a clip</li> <li>a user swipes up to the next clip</li> <li>a user swipes down to the previous clip</li> <li>a user is sent directly to a clip via a call to <code>openCollection</code></li> <li>a user is sent directly to a clip via a deep link</li> <li>a user opens a Category and navigates to a new Clip or moves back to a previous Category</li> <li>a user dismisses the last Category (by pressing back or swiping right) and returns to the top-level Collection</li> <li>a user opens embedded Clips for the first time.</li> <li>a user pulls to refresh at the top of Embedded Clips.</li> <li>a user is viewing a Collection with \"For You/Following\" enabled and switches between \"For You\" and \"Following\".</li> </ul> <p>Whenever an Opened Clip event occurs, additional event data specific to this event includes <code>openedReason</code>.</p>"},{"location":"Analytics/#dismissed-clip","title":"Dismissed Clip","text":"<p>This event is recorded when:</p> <ul> <li>a user taps on the back button in the top-left to exit the Clips player (and is at the top of the stack of Clip Categories)</li> </ul> <p>This event does not fire when the user is not at the top of the Clip Category stack. Whenever a Dismissed Clip event occurs, additional event data specific to this event includes <code>dismissedReason</code>, <code>durationViewed</code>, <code>clipsViewed</code>, and <code>loopsViewed</code>.</p>"},{"location":"Analytics/#next-clip","title":"Next Clip","text":"<p>This event is recorded when:</p> <ul> <li>a user swipes up to go to the next clip</li> </ul>"},{"location":"Analytics/#previous-clip","title":"Previous Clip","text":"<p>This event is recorded when:</p> <ul> <li>a user swipes down to go to the previous clip</li> </ul>"},{"location":"Analytics/#completed-loop","title":"Completed Loop","text":"<p>This event is recorded when:</p> <ul> <li>a user completes a loop of a clip</li> </ul> <p>Whenever a Completed Loop event occurs, additional event data specific to this event includes <code>completionType</code>.</p> <p>Note: For live clips this event will never be fired.</p>"},{"location":"Analytics/#action-button-tapped_1","title":"Action Button Tapped","text":"<p>This event is recorded when:</p> <ul> <li>a user taps the primary action button at the bottom of a Clip</li> <li>a user taps any secondary action buttons above the clip title</li> <li>a user swipes left on a clip to open the relevant action</li> </ul> <p>Whenever an Action Button Tapped event occurs, additional event data specific to this event includes <code>actionText</code>, <code>actionClass</code>, <code>actionIndex</code>, <code>tappedClipActionText</code>, <code>tappedClipActionUrl</code>, and <code>tappedClipActionType</code>.</p>"},{"location":"Analytics/#share-button-tapped_1","title":"Share Button Tapped","text":"<p>This event is recorded when:</p> <ul> <li>a user taps the share button on a clip</li> </ul>"},{"location":"Analytics/#share-success_1","title":"Share Success","text":"<p>This event is recorded when:</p> <ul> <li>a user selects and successfully shares a page from the system dialog</li> </ul> <p>Whenever a Share Success event occurs, additional event data specific to this event includes <code>shareMethod</code>.</p>"},{"location":"Analytics/#paused-clip","title":"Paused Clip","text":"<p>This event is recorded when:</p> <ul> <li>a user taps on the screen whilst a clip is playing to pause the clip - it does not fire when a clip is paused automatically by sharing or following an action</li> </ul> <p>Note: For live clips this event will never be fired, as pausing/resuming live clips is not supported.</p>"},{"location":"Analytics/#resumed-clip","title":"Resumed Clip","text":"<p>This event is recorded when:</p> <ul> <li>a user taps a clip that is paused to resume playback - it does not fire when a clip is resumed automatically</li> </ul> <p>Note: For live clips this event will never be fired, as pausing/resuming live clips is not supported.</p>"},{"location":"Analytics/#scrubbed-clip","title":"Scrubbed Clip","text":"<p>This event is recorded when:</p> <ul> <li>a user scrubs the clip to a different position by dragging the clip progress bar</li> </ul> <p>Whenever a Scrubbed Clip event occurs, additional event data specific to this event includes <code>openedReason</code>, <code>startPosition</code>, <code>endPosition</code>, and <code>scrubDirection</code>.</p>"},{"location":"Analytics/#liked-clip","title":"Liked Clip","text":"<p>This event is recorded when:</p> <ul> <li>a user likes a clip by tapping the like button when they do not currently like the clip</li> </ul>"},{"location":"Analytics/#unliked-clip","title":"Unliked Clip","text":"<p>This event is recorded when:</p> <ul> <li>a user unlikes a clip by tapping the like button when they currently like the clip</li> </ul>"},{"location":"Analytics/#finished-clip","title":"Finished Clip","text":"<p>This event is recorded when:</p> <ul> <li>Dismissed Clip is fired</li> <li>Next Clip is fired</li> <li>Previous Clip is fired</li> <li>Opened Category is fired</li> <li>The clip reaches the end and starts a new loop</li> <li>A user is viewing a Collection with \"For You/Following\" enabled and switches between \"For You\" and \"Following\"</li> </ul> <p>Whenever a Finished Clip event occurs, additional event data specific to this event includes <code>loopsViewed</code>, <code>durationViewed</code>, and <code>completionType</code>.</p>"},{"location":"Analytics/#opened-category","title":"Opened Category","text":"<p>This event is recorded when:</p> <ul> <li>a user taps a category at the bottom of the Clips Player to open a new Category</li> <li>a user opens a category by navigating back from another category</li> </ul> <p>Note: This event is not fired when opening the top level of a collection - only when navigating to a specific category.</p>"},{"location":"Analytics/#dismissed-category","title":"Dismissed Category","text":"<p>This event is recorded when:</p> <ul> <li>a user navigates back using the back button to the previous category.</li> </ul> <p>This event does not fire when dismissing the top level collection</p>"},{"location":"Analytics/#follow-category","title":"Follow Category","text":"<p>This event is recorded when:</p> <ul> <li>a user taps the plus button under the follow category button of the Clips Player</li> <li>a user taps the follow button at the top right of the Followable Category screen and the category was not followed</li> </ul>"},{"location":"Analytics/#unfollow-category","title":"Unfollow Category","text":"<p>This event is recorded when:</p> <ul> <li>a user taps the checkmark button under the follow category button of the Clips Player</li> <li>a user taps the follow button at the top right of the Followable Category screen and the category was followed</li> </ul>"},{"location":"Analytics/#followable-category-tapped","title":"Followable Category Tapped","text":"<p>This event is recorded when:</p> <ul> <li>a user taps the follow category button of the Clips Player to open the Followable Category screen</li> <li>a user swipes left from the right edge of the screen on a Clip with a Followable Category and opens the Followable Category screen</li> </ul>"},{"location":"Analytics/#enable-captions","title":"Enable Captions","text":"<p>This event is recorded when:</p> <ul> <li>a user taps the button to enable Closed Captions in the Clips Player</li> </ul>"},{"location":"Analytics/#disable-captions","title":"Disable Captions","text":"<p>This event is recorded when:</p> <ul> <li>a user taps the button to disable Closed Captions in the Clips Player</li> </ul>"},{"location":"Analytics/#muted-clip","title":"Muted Clip","text":"<p>This event is recorded when:</p> <ul> <li>A user mutes a Clip by toggling the mute button, adjusting device volume controls, or using the UI mute button</li> </ul>"},{"location":"Analytics/#unmuted-clip","title":"Unmuted Clip","text":"<p>This event is recorded when:</p> <ul> <li>A user unmutes a Clip by toggling the mute button, adjusting device volume controls, or using the UI mute button</li> </ul>"},{"location":"Analytics/#followable-category-limit-shown","title":"Followable Category Limit Shown","text":"<p>This event is recorded when:</p> <ul> <li>the alert dialogue box associated with the Followable Category Limit is shown</li> </ul> <p>Whenever a Followable Category Limit Shown event occurs, additional event data specific to this event includes <code>followableCategoryLimitDialogue</code>, <code>followableCategoryLimitActionText</code>, and <code>followableCategoryLimitActionUrl</code>.</p>"},{"location":"Analytics/#followable-category-limit-action-button-tapped","title":"Followable Category Limit Action Button Tapped","text":"<p>This event is recorded when:</p> <ul> <li>a user taps on the customisable action button in the Followable Category Limit Dialogue Box</li> </ul> <p>Whenever a Followable Category Limit Action Button Tapped event occurs, additional event data specific to this event includes <code>followableCategoryLimitDialogue</code>, <code>followableCategoryLimitActionText</code>, and <code>followableCategoryLimitActionUrl</code>.</p>"},{"location":"Analytics/#followable-category-limit-dismissed","title":"Followable Category Limit Dismissed","text":"<p>This event is recorded when:</p> <ul> <li>a user dismisses the Followable Category Limit Dialogue Box by tapping the cancel action button</li> </ul> <p>Whenever a Followable Category Limit Dismissed event occurs, additional event data specific to this event includes <code>followableCategoryLimitDialogue</code>, <code>followableCategoryLimitActionText</code>, and <code>followableCategoryLimitActionUrl</code>.</p>"},{"location":"Analytics/#list-tile-events","title":"List Tile Events","text":""},{"location":"Analytics/#tile-visible","title":"Tile Visible","text":"<p>This event is recorded when a Story or Clip tile becomes visible in a Storyteller list row or grid.</p> <p>It is recorded in the following scenarios:</p> <ul> <li>when list data is rendered and one or more tiles are visible</li> <li>when a user scrolls and new tiles become visible</li> <li>when a list reloads or refreshes with updated content and visible tiles are re-evaluated</li> </ul> <p>This event is not recorded again while the same tile remains continuously visible, and it is not recorded just because the app returns from the background or the Story/Clip Player is dismissed if the same visible tiles remain and the list did not reload or scroll.</p> <p>Whenever a Tile Visible event occurs, additional event data specific to this event includes <code>tileIndex</code>. Story tile events include <code>storyId</code>, <code>storyTitle</code>, <code>categories</code>, and <code>context</code> when available. Clip tile events include <code>clipId</code>, <code>clipTitle</code>, <code>collection</code>, <code>categories</code>, and <code>context</code> when available.</p>"},{"location":"Analytics/#card-events","title":"Card Events","text":"<p>The following properties are included in all Card-related events:</p> <ul> <li><code>cardActionType</code></li> <li><code>cardActionUrl</code></li> <li><code>cardAspectRatio</code></li> <li><code>cardBackgroundType</code></li> <li><code>cardCollectionId</code></li> <li><code>cardId</code></li> <li><code>cardIndex</code></li> <li><code>cardSubtitle</code></li> <li><code>cardTitle</code></li> <li><code>categories</code></li> <li><code>categoryDetails</code></li> <li><code>hasButton</code></li> <li><code>isLive</code></li> <li><code>context</code></li> <li><code>actionLinkId</code></li> <li><code>isMuted</code> when the Card audio state is known. Image and non-video Card events report <code>nil</code>.</li> </ul> <p>The following additional properties are included on specific card video events:</p> <ul> <li><code>videoStartReason</code> on <code>Card Video Started</code></li> <li><code>durationViewed</code> on <code>Card Video Heartbeat</code> and <code>Card Video Completed</code></li> <li><code>audioToggleFrom</code> and <code>audioToggleTo</code> on <code>Card Audio Toggled</code></li> </ul>"},{"location":"Analytics/#card-viewed","title":"Card Viewed","text":"<p>This event is recorded when:</p> <ul> <li>a Storyteller Card appears on screen, either by scrolling to it or opening a view where its visible</li> </ul>"},{"location":"Analytics/#card-tapped","title":"Card Tapped","text":"<p>This event is recorded when:</p> <ul> <li>a user taps on the Storyteller Card view</li> </ul>"},{"location":"Analytics/#card-video-started","title":"Card Video Started","text":"<p>This event is recorded when:</p> <ul> <li>a video Storyteller Card begins video playback</li> <li>a visible video Storyteller Card naturally loops and starts the next playback cycle</li> </ul> <p>This event is only emitted for video cards.</p> <p>Whenever a Card Video Started event occurs, additional event data specific to this event includes <code>videoStartReason</code>.</p> <p>For Card Video Started, <code>videoStartReason</code> is <code>natural</code> for the first visible playback start of the current Card session and <code>loop</code> when the same visible Card naturally restarts after a completed loop.</p>"},{"location":"Analytics/#card-video-heartbeat","title":"Card Video Heartbeat","text":"<p>This event is recorded when:</p> <ul> <li>a visible video Storyteller Card reaches 5 seconds, 10 seconds, 15 seconds, and so on of active playback within the current loop</li> </ul> <p>This event is only emitted for video cards.</p> <p>Whenever a Card Video Heartbeat event occurs, additional event data specific to this event includes <code>durationViewed</code>.</p> <p>For Card Video Heartbeat, <code>durationViewed</code> is reported in milliseconds, counts active playback only, accumulates through pauses, buffering, backgrounding, and other interruptions within the same loop, and resets when the video starts a new loop.</p>"},{"location":"Analytics/#card-video-completed","title":"Card Video Completed","text":"<p>This event is recorded when:</p> <ul> <li>a visible video Storyteller Card reaches a natural end-of-item loop boundary</li> </ul> <p>This event is only emitted for video cards.</p> <p>Whenever a Card Video Completed event occurs, additional event data specific to this event includes <code>durationViewed</code>.</p> <p>For Card Video Completed, <code>durationViewed</code> is the active playback duration of the completed loop in milliseconds.</p>"},{"location":"Analytics/#card-audio-toggled","title":"Card Audio Toggled","text":"<p>This event is recorded when:</p> <ul> <li>a user toggles audio for an active video Storyteller Card</li> </ul> <p>This event is only emitted for video Cards where audio can be controlled. It is not emitted for no-audio Cards, behavior-locked muted Cards, inactive Cards, or interruption-forced mutes.</p> <p>Whenever a Card Audio Toggled event occurs, additional event data specific to this event includes <code>audioToggleFrom</code>, <code>audioToggleTo</code>, and <code>isMuted</code>. <code>isMuted</code> reflects the resulting state after the toggle.</p>"},{"location":"Analytics/#ad-events","title":"Ad Events","text":"<p>Ad-related events include both ad interaction events (for example <code>OpenedAd</code>, <code>DismissedAd</code>) and paid ad operational events (for example <code>AdOpportunity</code>, <code>AdRequested</code>). Not every field is present on every ad event type.</p> <p>The following properties may be included in ad-related events:</p> <ul> <li><code>adFormat</code></li> <li><code>adId</code></li> <li><code>adIndex</code></li> <li><code>adPlacement</code></li> <li><code>adResponseIdentifier</code></li> <li><code>adStrategy</code></li> <li><code>adType</code></li> <li><code>adUnitId</code></li> <li><code>advertiserName</code></li> <li><code>airplayEnabled</code></li> <li><code>categories</code></li> <li><code>categoryDetails</code></li> <li><code>clipActionText</code></li> <li><code>clipActionType</code></li> <li><code>clipHasAction</code></li> <li><code>contentLength</code></li> <li><code>currentCategory</code></li> <li><code>isMuted</code></li> <li><code>pageActionText</code></li> <li><code>pageActionType</code></li> <li><code>pageActionUrl</code></li> <li><code>pageHasAction</code></li> <li><code>searchTerm</code></li> <li><code>context</code></li> </ul> <p>For Ad Opportunity events, additional fields include:</p> <ul> <li><code>playerSessionId</code></li> <li><code>adSlotType</code></li> </ul> <p>For Ad Requested events, additional fields include:</p> <ul> <li><code>playerSessionId</code></li> <li><code>adRequestId</code></li> <li><code>adSlotType</code></li> <li><code>adUnitId</code></li> <li><code>adSource</code></li> </ul> <p>For Ad Loaded events, additional fields include:</p> <ul> <li><code>playerSessionId</code></li> <li><code>adRequestId</code></li> <li><code>adSlotType</code></li> <li><code>adUnitId</code></li> <li><code>adSource</code></li> <li><code>adSourceName</code></li> <li><code>adSourceId</code></li> <li><code>adSourceInstanceName</code></li> <li><code>adSourceInstanceId</code></li> <li><code>adAdapterLatencyMillis</code></li> <li><code>adMediationGroupName</code></li> <li><code>adMediationAbTestName</code></li> <li><code>adMediationAbTestVariant</code></li> </ul> <p>For Ad Failed To Load events, additional fields include:</p> <ul> <li><code>playerSessionId</code></li> <li><code>adRequestId</code></li> <li><code>adSlotType</code></li> <li><code>adUnitId</code></li> <li><code>adSource</code></li> <li><code>adErrorCode</code></li> <li><code>adErrorDomain</code></li> <li><code>adErrorMessage</code></li> <li><code>adAdapterLatencyMillis</code></li> </ul> <p>For Ad Paid events, additional fields include:</p> <ul> <li><code>playerSessionId</code></li> <li><code>adRequestId</code></li> <li><code>adSlotType</code></li> <li><code>adUnitId</code></li> <li><code>adSource</code></li> <li><code>adSourceName</code></li> <li><code>adSourceId</code></li> <li><code>adSourceInstanceName</code></li> <li><code>adSourceInstanceId</code></li> <li><code>adAdapterLatencyMillis</code></li> <li><code>adValueMicros</code></li> <li><code>adCurrencyCode</code></li> <li><code>adValuePrecision</code></li> </ul> <p>For Story Ad events specifically, additional common fields include:</p> <ul> <li><code>storyId</code></li> <li><code>pageId</code></li> <li><code>pageActionText</code></li> <li><code>pageActionType</code></li> <li><code>pageActionUrl</code></li> <li><code>pageHasAction</code></li> <li><code>pageType</code></li> <li><code>storyPlaybackMode</code></li> </ul> <p>For Clip Ad events specifically, additional common fields include:</p> <ul> <li><code>clipId</code></li> <li><code>clipActionType</code></li> <li><code>clipHasAction</code></li> <li><code>clipActionText</code></li> <li><code>clipActionUrl</code></li> <li><code>clipFeedType</code></li> <li><code>clipIndex</code></li> <li><code>collection</code></li> <li><code>collectionClipCount</code></li> <li><code>loopsViewed</code></li> </ul>"},{"location":"Analytics/#ad-opportunity","title":"Ad Opportunity","text":"<p>This event is recorded when the SDK reaches an eligible ad slot and is about to request an ad.</p> <p>This can happen for:</p> <ul> <li>full-screen ads in Stories</li> <li>full-screen ads between clips</li> <li>bottom banner ads in the Clips player</li> </ul> <p>Whenever an Ad Opportunity event occurs, additional event data specific to this event includes <code>playerSessionId</code> and <code>adSlotType</code>.</p> <p><code>AdOpportunity</code> also includes the same contextual ad fields as <code>OpenedAd</code> when they are available at opportunity time (for example <code>storyId</code>, <code>pageId</code>, <code>storyPlaybackMode</code>, <code>categories</code>, <code>currentCategory</code>, <code>pageActionType</code>, <code>pageHasAction</code>, <code>pageActionText</code>, <code>pageActionUrl</code>, <code>clipId</code>, <code>clipActionType</code>, <code>clipHasAction</code>, <code>clipActionText</code>, <code>clipActionUrl</code>).</p> <p>Some differences between <code>AdOpportunity</code> and <code>OpenedAd</code> are expected by design because they are captured at different moments: <code>AdOpportunity</code> is a slot-time snapshot (before the ad request), while <code>OpenedAd</code> is captured when the rendered ad is opened.</p>"},{"location":"Analytics/#ad-requested","title":"Ad Requested","text":"<p>This event is recorded when the SDK initiates an Ad request. This event is emitted only for Google Ad sources (GAM and AdMob).</p> <p>This can happen for:</p> <ul> <li>full-screen ads in Stories</li> <li>full-screen ads between clips</li> <li>bottom banner ads in the Clips player</li> </ul> <p>Whenever an Ad Requested event occurs, additional event data specific to this event includes <code>playerSessionId</code>, <code>adRequestId</code>, <code>adSlotType</code>, <code>adUnitId</code>, and <code>adSource</code>.</p> <p>For GAM and AdMob integrations supplied by Storyteller, <code>adUnitId</code> is the exact unit used for that request attempt. If AdMob falls back between native and banner formats, each concrete attempt emits its own <code>AdRequested</code> event with the corresponding unit and the same <code>adRequestId</code>.</p> <p>If a Google module fails before it can resolve a unit or start a concrete provider load, the SDK preserves the request/failure event pair with <code>adUnitId</code> set to <code>nil</code>.</p>"},{"location":"Analytics/#ad-loaded","title":"Ad Loaded","text":"<p>This event is recorded when the SDK successfully loads an ad and considers it available for rendering (even if it is not ultimately shown). This event is emitted only for Google Ad sources (GAM and AdMob).</p> <p>This can happen for:</p> <ul> <li>full-screen ads in Stories</li> <li>full-screen ads between clips</li> <li>bottom banner ads in the Clips player</li> </ul> <p>Whenever an Ad Loaded event occurs, additional event data specific to this event includes <code>playerSessionId</code>, <code>adRequestId</code>, <code>adSlotType</code>, <code>adUnitId</code>, <code>adSource</code>, <code>adSourceName</code>, <code>adSourceId</code>, <code>adSourceInstanceName</code>, <code>adSourceInstanceId</code>, <code>adAdapterLatencyMillis</code>, <code>adMediationGroupName</code>, <code>adMediationAbTestName</code>, and <code>adMediationAbTestVariant</code>.</p> <p><code>adUnitId</code> identifies the successful unit. Response metadata takes precedence when it differs from the requested unit.</p>"},{"location":"Analytics/#ad-failed-to-load","title":"Ad Failed To Load","text":"<p>This event is recorded when the SDK fails to load an Ad request (including no-fill). This event is emitted only for Google Ad sources (GAM and AdMob).</p> <p>This can happen for:</p> <ul> <li>full-screen ads in Stories</li> <li>full-screen ads between clips</li> <li>bottom banner ads in the Clips player</li> </ul> <p>Whenever an Ad Failed To Load event occurs, additional event data specific to this event includes <code>playerSessionId</code>, <code>adRequestId</code>, <code>adSlotType</code>, <code>adUnitId</code>, <code>adSource</code>, <code>adErrorCode</code>, <code>adErrorDomain</code>, <code>adErrorMessage</code>, and <code>adAdapterLatencyMillis</code>.</p> <p><code>adUnitId</code> identifies the concrete unit used by the failed request attempt. It is <code>nil</code> when the module failed before resolving a unit or starting the provider load.</p>"},{"location":"Analytics/#ad-paid","title":"Ad Paid","text":"<p>This event is recorded when the SDK receives impression-level paid revenue info for an Ad from Google Ad sources.</p> <p>This can happen for:</p> <ul> <li>full-screen ads in Stories</li> <li>full-screen ads between clips</li> <li>bottom banner ads in the Clips player</li> </ul> <p>Whenever an Ad Paid event occurs, additional event data specific to this event includes <code>playerSessionId</code>, <code>adRequestId</code>, <code>adSlotType</code>, <code>adUnitId</code>, <code>adSource</code>, <code>adSourceName</code>, <code>adSourceId</code>, <code>adSourceInstanceName</code>, <code>adSourceInstanceId</code>, <code>adAdapterLatencyMillis</code>, <code>adValueMicros</code>, <code>adCurrencyCode</code>, and <code>adValuePrecision</code>.</p>"},{"location":"Analytics/#ad-session-summary","title":"Ad Session Summary","text":"<p>This event is recorded once when a root Storyteller player session ends (Clips or Stories). If a prior session was persisted without being ended cleanly, this summary can also be emitted on the next SDK initialize. The session spans app background/foreground transitions and is not restarted until the root player session ends.</p> <p>The summary aggregates paid ads activity for that player session:</p> <ul> <li><code>playerSessionId</code></li> <li><code>adSessionOpportunitiesCount</code></li> <li><code>adSessionRequestsCount</code></li> <li><code>adSessionLoadsCount</code></li> <li><code>adSessionFailedToLoadCount</code></li> <li><code>adSessionPaidCount</code></li> <li><code>adSessionRevenueMicros</code> (optional)</li> <li><code>adSessionCurrencyCode</code> (optional)</li> </ul> <p><code>adSessionRevenueMicros</code> and <code>adSessionCurrencyCode</code> are only included when all paid events in that session can be represented as a single currency with complete paid data.</p> <p>Whenever an Ad Session Summary event occurs, additional event data specific to this event includes <code>playerSessionId</code>, <code>adSessionOpportunitiesCount</code>, <code>adSessionRequestsCount</code>, <code>adSessionLoadsCount</code>, <code>adSessionFailedToLoadCount</code>, <code>adSessionPaidCount</code>, <code>adSessionRevenueMicros</code> (optional), and <code>adSessionCurrencyCode</code> (optional).</p> <p><code>adUnitId</code> is <code>nil</code> for <code>AdSessionSummary</code> because a session can aggregate requests across multiple ad units.</p>"},{"location":"Analytics/#opened-ad","title":"Opened Ad","text":""},{"location":"Analytics/#stories","title":"Stories","text":"<p>This event is recorded when:</p> <ul> <li>an ad is loaded because the previous story finished</li> <li>a user swipes left on a story to go to the next story and an ad appears</li> <li>a user swipes right on a story to go to the previous story and an ad appears</li> </ul> <p>Whenever an Opened Ad event occurs, additional event data specific to this event includes <code>adView</code> and <code>openedReason</code>.</p>"},{"location":"Analytics/#clips","title":"Clips","text":"<p>This event is recorded when:</p> <ul> <li>a user swipes up on a clip to go to the next clip and an ad should appear next</li> <li>a user swipes down on a clip to go to the previous clip and an ad should appear next</li> <li>an ad completes a loop and begins playing again from the start</li> </ul> <p>Information about Ad View is only sent to client delegates on an OpenedAd event.</p> <p>Whenever an Opened Ad event occurs, additional event data specific to this event includes <code>adView</code> and <code>openedReason</code>.</p>"},{"location":"Analytics/#dismissed-ad","title":"Dismissed Ad","text":""},{"location":"Analytics/#stories_1","title":"Stories","text":"<p>This event is recorded when a user:</p> <ul> <li>taps close to dismiss the ad</li> <li>swipes down to dismiss the ad</li> <li>taps back on their device UI to dismiss the ad (Android only)</li> <li>taps to skip the ad if the ad is the last page in the current set of stories</li> <li>swipes left on the ad to skip it if the ad is the last page in the current set of stories</li> <li>completes the ad if the ad is the last page in the current set of stories</li> </ul> <p>Whenever a Dismissed Ad event occurs, additional event data specific to this event includes <code>durationViewed</code>, <code>dismissedReason</code> and <code>pagesViewedCount</code>.</p>"},{"location":"Analytics/#clips_1","title":"Clips","text":"<p>This event is recorded when a user:</p> <ul> <li>taps the back button to exit the clips player when an ad is being shown</li> </ul> <p>Whenever a Dismissed Ad event occurs, additional event data specific to this event includes <code>durationViewed</code>, and <code>clipsViewed</code>.</p>"},{"location":"Analytics/#skipped-ad","title":"Skipped Ad","text":""},{"location":"Analytics/#stories_2","title":"Stories","text":"<p>This event is recorded when a user:</p> <ul> <li>swipes left to go to the next story before completing the current ad</li> <li>taps to go to the next page on an ad before completing the current ad</li> </ul>"},{"location":"Analytics/#clips_2","title":"Clips","text":"<p>This event is recorded when:</p> <ul> <li>a user swipes up to go to the next clip</li> <li>a user swipes down to go the previous clip</li> </ul>"},{"location":"Analytics/#ad-action-button-tapped","title":"Ad Action Button Tapped","text":""},{"location":"Analytics/#stories_3","title":"Stories","text":"<p>This event is recorded when a user:</p> <ul> <li>swipes up on an ad to open a link</li> <li>taps on the swipe up element of an ad to open a link</li> </ul>"},{"location":"Analytics/#clips_3","title":"Clips","text":"<p>This event is recorded when a user:</p> <ul> <li>taps the action button at the bottom of an ad displayed in clips</li> <li>swipes left on a clip to open the relevant action</li> </ul>"},{"location":"Analytics/#finished-ad","title":"Finished Ad","text":""},{"location":"Analytics/#stories_4","title":"Stories","text":"<p>This event is recorded at the same time as Dismissed Ad, Skipped Ad and Viewed Ad Page Complete and gives an easier way to determine when an ad finishes for any reason.</p> <p>Whenever a Finished Ad event occurs, additional event data specific to this event includes <code>adView</code>.</p>"},{"location":"Analytics/#clips_4","title":"Clips","text":"<p>This event is recorded at the same time as DismissedAd, or SkippedAd.</p> <p>Whenever a Finished Ad event occurs, additional event data specific to this event includes <code>adView</code>.</p>"},{"location":"Analytics/#paused-ad","title":"Paused Ad","text":""},{"location":"Analytics/#stories_5","title":"Stories","text":"<p>This event is recorded when a user pauses a page within an ad by pressing and holding on the page.</p>"},{"location":"Analytics/#clips_5","title":"Clips","text":"<p>This event is recorded when a user pauses a clip which is an ad by tapping the screen. It does not fire when a clip is paused automatically for sharing or following an action.</p>"},{"location":"Analytics/#resumed-ad","title":"Resumed Ad","text":""},{"location":"Analytics/#stories_6","title":"Stories","text":"<p>This event is recorded when a user resumes playing a page within an ad by releasing their long press which paused the ad.</p>"},{"location":"Analytics/#clips_6","title":"Clips","text":"<p>This event is fired when a clip which is an ad is paused and a user taps the screen to resume playback. It does not fire when a clip is resumed automatically.</p>"},{"location":"Analytics/#viewed-ad-page-first-quartile","title":"Viewed Ad Page First Quartile","text":"<p>This event is recorded when a user reaches 1/4 of the way through the duration of a page or clip which is an ad.</p> <p>For clip ads (which loop), it fires for each loop of the clip.</p>"},{"location":"Analytics/#viewed-ad-page-midpoint","title":"Viewed Ad Page Midpoint","text":"<p>This event is recorded when a user reaches halfway through the duration of a page or clip which is an ad.</p> <p>For clip ads (which loop), it fires for each loop of the clip.</p>"},{"location":"Analytics/#viewed-ad-page-third-quartile","title":"Viewed Ad Page Third Quartile","text":"<p>This event is recorded when a user reaches 3/4 of the way through the duration of a page or clip which is an ad.</p> <p>For clip ads (which loop), it fires for each loop of the clip.</p>"},{"location":"Analytics/#viewed-ad-page-complete","title":"Viewed Ad Page Complete","text":"<p>This event is recorded when a user reaches the end of the duration for a page or clip which is an ad.</p> <p>For clip ads (which loop), it fires for each loop of the clip.</p>"},{"location":"Analytics/#muted-ad","title":"Muted Ad","text":"<p>This event is recorded when:</p> <ul> <li>A user mutes an Ad (in either Stories or Clips) by toggling the mute button, adjusting device volume controls, or using the UI mute button</li> </ul>"},{"location":"Analytics/#unmuted-ad","title":"Unmuted Ad","text":"<p>This event is recorded when:</p> <ul> <li>A user unmutes an Ad (in either Stories or Clips) by toggling the mute button, adjusting device volume controls, or using the UI mute button</li> </ul>"},{"location":"Analytics/#playback-events","title":"Playback Events","text":"<p>The following properties are included in all Playback-related events:</p> <ul> <li><code>isMuted</code></li> <li><code>pageActionText</code></li> <li><code>pageActionUrl</code></li> <li><code>pageHasAction</code></li> <li><code>pageId</code></li> <li><code>pageIndex</code></li> <li><code>pageTitle</code></li> <li><code>pageType</code></li> <li><code>storyDisplayTitle</code></li> <li><code>storyId</code></li> <li><code>storyIndex</code></li> <li><code>storyPlaybackMode</code></li> <li><code>storyTitle</code></li> <li><code>context</code></li> </ul>"},{"location":"Analytics/#ready-to-play","title":"Ready to Play","text":"<p>This event is called once per video page at the point when the video player has been loaded.</p>"},{"location":"Analytics/#media-started","title":"Media Started","text":"<p>This event is called once per video page at the point when the video starts to play for the first time.</p>"},{"location":"Analytics/#buffering-started","title":"Buffering Started","text":"<p>This event is called on video pages whenever the video starts to buffer.</p> <p>Whenever a Buffering Started event occurs, additional event data specific to this event includes <code>isInitialBuffering</code>.</p>"},{"location":"Analytics/#buffering-ended","title":"Buffering Ended","text":"<p>This event is called on video pages whenever the video finishes buffering.</p> <p>Note: There should be at most one Ready to Play event and one Media Started event for every page. There may be multiple Buffering Started/Buffering Ended pairs of events for an individual page. There may not always be a Buffering Ended event for every Buffering Started event as the user may choose to exit the page during buffering.</p> <p>Whenever a Buffering Ended event occurs, additional event data specific to this event includes <code>isInitialBuffering</code> and <code>timeSinceBufferingBegan</code>.</p>"},{"location":"Analytics/#sheet-events","title":"Sheet Events","text":"<p>The following properties are included in all Sheet-related events:</p> <ul> <li><code>actionText</code></li> <li><code>captionsEnabled</code></li> <li><code>categories</code></li> <li><code>categoryDetails</code></li> <li><code>clipActionText</code></li> <li><code>clipActionType</code></li> <li><code>clipActionUrl</code></li> <li><code>clipFeedType</code></li> <li><code>clipHasAction</code></li> <li><code>clipId</code></li> <li><code>clipIndex</code></li> <li><code>clipTitle</code></li> <li><code>collection</code></li> <li><code>collectionClipCount</code></li> <li><code>containerTitle</code></li> <li><code>currentCategory</code></li> <li><code>isLive</code></li> <li><code>isMuted</code></li> <li><code>pageActionText</code></li> <li><code>pageActionType</code></li> <li><code>pageActionUrl</code></li> <li><code>pageHasAction</code></li> <li><code>pageId</code></li> <li><code>pageIndex</code></li> <li><code>pageTitle</code></li> <li><code>pageType</code></li> <li><code>searchFilter</code></li> <li><code>searchSort</code></li> <li><code>searchTerm</code></li> <li><code>sheetId</code></li> <li><code>sheetSize</code></li> <li><code>sheetTitle</code></li> <li><code>storyId</code></li> <li><code>storyIndex</code></li> <li><code>storyPageCount</code></li> <li><code>storyTitle</code></li> <li><code>context</code></li> </ul>"},{"location":"Analytics/#opened-sheet","title":"Opened Sheet","text":"<p>This event is recorded when a user opens a Sheet.</p> <p>Whenever an Opened Sheet event occurs, additional event data specific to this event includes <code>openedReason</code>.</p>"},{"location":"Analytics/#dismissed-sheet","title":"Dismissed Sheet","text":"<p>This event is recorded when a user closes a Sheet.</p>"},{"location":"Analytics/#search-events","title":"Search Events","text":"<p>The following properties are included in all Search-related events:</p> <ul> <li><code>categories</code></li> <li><code>categoryDetails</code></li> <li><code>currentCategory</code></li> <li><code>isSuggestion</code></li> <li><code>searchFilter</code></li> <li><code>searchFrom</code></li> <li><code>searchSort</code></li> <li><code>searchTerm</code></li> </ul> <p>When search is opened from a Story context, additional properties include:</p> <ul> <li><code>pageActionText</code></li> <li><code>pageActionUrl</code></li> <li><code>pageHasAction</code></li> <li><code>pageId</code></li> <li><code>pageIndex</code></li> <li><code>pageTitle</code></li> <li><code>pageType</code></li> <li><code>storyDisplayTitle</code></li> <li><code>storyId</code></li> <li><code>storyIndex</code></li> <li><code>storyPageCount</code></li> <li><code>storyPlaybackMode</code></li> <li><code>storyReadStatus</code></li> <li><code>storyTitle</code></li> <li><code>context</code></li> </ul> <p>When search is opened from a Clip context, additional properties include:</p> <ul> <li><code>clipActionText</code></li> <li><code>clipActionUrl</code></li> <li><code>clipHasAction</code></li> <li><code>clipId</code></li> <li><code>clipIndex</code></li> <li><code>clipTitle</code></li> <li><code>collection</code></li> <li><code>context</code></li> </ul>"},{"location":"Analytics/#opened-search","title":"Opened Search","text":"<p>The openedSearch event is recorded when:</p> <ul> <li>A user taps on Search from a Story within the Story player</li> <li>A user taps on Search from a Clip within the Clip player</li> <li>The <code>Storyteller.shared.openSearch()</code> function is called</li> </ul>"},{"location":"Analytics/#dismissed-search","title":"Dismissed Search","text":"<p>The dismissedSearch event is recorded when a user taps the 'X' button to exit the Search interface.</p> <p>Whenever an Dismissed Search event occurs, additional event data specific to this event includes <code>dismissedReason</code>.</p>"},{"location":"Analytics/#performed-search","title":"Performed Search","text":"<p>The performedSearch event is recorded when:</p> <ul> <li>A user taps the 'Search' icon after entering a term in the Search bar (whether manually or by tapping the 'arrow' icon beside a Search suggestion to populate the search bar)</li> <li>A user taps the 'Search' icon beside a Search suggestion. The Search is then performed with the suggestion as the term.</li> <li>A user taps 'Apply filters' from the filters interface.</li> </ul>"},{"location":"Analytics/#opened-filters","title":"Opened Filters","text":"<p>The openedFilters event is recorded when, after a user has performed as Search, they press the 'Filter' icon to bring up the filter interface.</p> <p>Note: That filters can only be applied after the initial search has been performed.</p>"},{"location":"Analytics/#dismissed-filters","title":"Dismissed Filters","text":"<p>This event is recorded when a user, after calling up the 'Filters' interface, swipes down to exit the interface without applying any.</p>"},{"location":"Analytics/#used-suggestion","title":"Used Suggestion","text":"<p>The usedSuggestion event is recorded when:</p> <ul> <li>A user taps the 'arrow' icon beside a Search suggestion to populate the Search bar. This does not trigger any other event, and the user may amend the Search bar input before performing a Search.</li> <li>A user taps the 'Search' icon beside a Search suggestion. This simultaneously triggers a PerformedSearch event, above.</li> </ul> <p>Whenever a Used Suggestion event occurs, additional event data specific to this event includes <code>initialInput</code>.</p>"},{"location":"Analytics/#event-data","title":"Event Data","text":"<p>For each event, data is returned with details about the story and page involved as well as some extra properties with more information about what the user has done. The data is returned as a <code>StorytellerUserActivityData</code> class with the following properties:</p>"},{"location":"Analytics/#story-id","title":"Story ID","text":"<p>The <code>storyId: String?</code> is the ID of the story for which the event occurred.</p>"},{"location":"Analytics/#story-title","title":"Story Title","text":"<p>The <code>storyTitle: String?</code> is the title of the story for which the event occurred.</p>"},{"location":"Analytics/#story-display-title","title":"Story Display Title","text":"<p>The <code>storyDisplayTitle: String?</code> is the display title of the Story for which the event occurred.</p>"},{"location":"Analytics/#story-index","title":"Story Index","text":"<p>The <code>storyIndex: Int?</code> is the index of the story for which the event occurred in the row from which it was opened at the point it was opened - this is only included on <code>OpenedStory</code> events.</p> <p>Note: This value is 1-based.</p>"},{"location":"Analytics/#story-page-count","title":"Story Page Count","text":"<p>The <code>storyPageCount: Int?</code> is the number of pages in the story.</p>"},{"location":"Analytics/#story-read-status","title":"Story Read Status","text":"<p>The <code>storyReadStatus: String?</code> is whether the story was read or unread at the point the story was opened - this is only included on <code>OpenedStory</code> events.</p> <p>Note: This will either be <code>read</code> or <code>unread</code>.</p>"},{"location":"Analytics/#page-id","title":"Page ID","text":"<p>The <code>pageId: String?</code> is the ID of the page for which the event occurred.</p>"},{"location":"Analytics/#page-index","title":"Page Index","text":"<p>The <code>pageIndex: Int?</code> is the index of the page in the story for which the event occurred.</p> <p>Note: This value is 1-based.</p>"},{"location":"Analytics/#page-type","title":"Page Type","text":"<p>The <code>pageType: String?</code> is the type of the page associated with the event. This can have the value <code>image</code>, <code>video</code> or <code>poll</code>.</p>"},{"location":"Analytics/#story-playback-mode","title":"Story Playback Mode","text":"<p>The <code>storyPlaybackMode: String?</code> value states if the story was opened during the list or in the single story mode (Storyteller static method.) This is included for all events. The values for this are either <code>list</code> or <code>singleStory</code>.</p>"},{"location":"Analytics/#page-has-action","title":"Page Has Action","text":"<p>The <code>pageHasAction: Bool?</code> value states whether the page associated with the event contains an action.</p>"},{"location":"Analytics/#page-action-type","title":"Page Action Type","text":"<p>The <code>pageActionType: String?</code> is the type of the action on the page.</p>"},{"location":"Analytics/#page-action-text","title":"Page Action Text","text":"<p>The <code>pageActionText: String?</code> is the text call to action if the page has an action.</p>"},{"location":"Analytics/#page-action-url","title":"Page Action URL","text":"<p>The <code>pageActionUrl: String?</code> is the URL for the link if the page has an action.</p>"},{"location":"Analytics/#action-link-id","title":"Action Link ID","text":"<p>The <code>actionLinkId: String?</code> is the unique identifier of the action associated with the current story page, clip or card. This is not included for Ad events.</p>"},{"location":"Analytics/#opened-reason","title":"Opened Reason","text":"<p>The <code>openedReason: String?</code> value states how the user opened a Story or Clip. The possible values for this are: - <code>storyListTap</code>: The user tapped the Story in the Story row. - <code>clipListTap</code>: The user tapped the Clip in a Clip list. - <code>deepLink</code>: <code>openStory</code> or <code>openPage</code> was called to open the Story. - <code>swipe</code>: The user swiped left or right to change the current Story. - <code>automaticPlayback</code>: The user completed the previous Page. - <code>card</code>: The user taps on a Card. - <code>clipActionButton</code>: The user clicked on an action button in the Clips player. - <code>pageActionButton</code>: The user clicked on the action button in the Story player. - <code>tap</code>: The user tapped on the next or previous Story Page to navigate to this page. - <code>instanceMethod</code>: <code>Storyteller.shared.openStory(id:)</code> or <code>Storyteller.shared.openPage(id:)</code> was called to open a Story or a Page. - <code>loop</code>: The user completes a loop of a Clip naturally or by scrubbing to the end of the Clip's duration.</p> <p><code>openedReason</code> is only included on <code>OpenedStory</code>, <code>OpenedClip</code>, <code>OpenedPage</code>, <code>StoryInstructionsScreenViewed</code>, <code>CompletedLoop</code>, <code>OpenedAd</code> and <code>OpenedSheet</code> events.</p>"},{"location":"Analytics/#video-start-reason","title":"Video Start Reason","text":"<p>The <code>videoStartReason: String?</code> value states why a Card video start was emitted. The possible values are <code>natural</code> (the first visible playback start for the Card session) and <code>loop</code> (the Card video completed a loop and restarted while still visible).</p> <p><code>videoStartReason</code> is only included on <code>CardVideoStarted</code> events.</p>"},{"location":"Analytics/#dismissed-reason","title":"Dismissed Reason","text":"<p>The <code>dismissedReason: String?</code> value states the way the user dismissed a story or clip. The possible values for this are <code>closeButtonTapped</code> (the user tapped close to dismiss the story); <code>swipedDown</code> (the user swiped down to dismiss the story); <code>swipedFirstStory</code> (the user swiped the first story to dismiss it); <code>swipedFinalStory</code> (the user swiped the final story to dismiss it); <code>skippedFinalPage</code> (the user tapped to skip the final page of the final story); <code>completedFinalPage</code> (the user completed the final page of the final story) and <code>backButtonTapped</code> (the user tapped the back button to dismiss the clip).</p> <p><code>dismissedReason</code> is only included on <code>DismissedStory</code> and <code>DismissedClip</code> events.</p>"},{"location":"Analytics/#duration-viewed","title":"Duration Viewed","text":"<p>The <code>durationViewed: Float?</code> is the duration the user viewed the story or clip for in milliseconds. This is measured from the most recent <code>OpenedStory</code> or <code>OpenedClip</code> event with an Opened Reason of <code>storyRowTap</code>, <code>deepLink</code>, <code>card</code>, <code>pageActionButton</code>, <code>clipActionButton</code> or <code>clipsListTap</code>.</p> <p>This timer is reset after any <code>DismissedStory</code> or <code>DismissedClip</code> events.</p> <p>For <code>FinishedClip</code>, Duration Viewed is the duration the user viewed the clips player for in milliseconds. This is measured from the most recent <code>OpenedClip</code> event with an Opened Reason of <code>swipe</code>.</p> <p>For <code>CardVideoHeartbeat</code> and <code>CardVideoCompleted</code>, <code>durationViewed</code> is the active playback duration for the current card-video loop in milliseconds. It accumulates through pauses, buffering, backgrounding, and other interruptions within the same loop, and resets when a new loop starts.</p>"},{"location":"Analytics/#audio-toggle-from-to","title":"Audio Toggle From / To","text":"<p>The <code>audioToggleFrom: String?</code> and <code>audioToggleTo: String?</code> values state the previous and resulting Cards audio state for <code>CardAudioToggled</code> events. Possible values are <code>muted</code> and <code>unmuted</code>.</p>"},{"location":"Analytics/#pages-viewed-count","title":"Pages Viewed Count","text":"<p>The <code>pagesViewedCount: Int?</code> is the total number of pages a user has viewed since the most recent <code>OpenedStory</code> event with an Opened Reason of <code>storyRowTap</code>, <code>pageActionButton</code>, <code>clipActionButton</code>, <code>card</code> or <code>deepLink</code>. This count is reset after any <code>DismissedStory</code> events.</p>"},{"location":"Analytics/#content-length","title":"Content Length","text":"<p>The <code>contentLength: Int?</code> is the total duration of the page content in seconds.</p>"},{"location":"Analytics/#share-method","title":"Share Method","text":"<p>The <code>shareMethod: String?</code> is the component name of the app which the user has selected for sharing.</p>"},{"location":"Analytics/#advertisers-name","title":"Advertisers Name","text":"<p>The <code>advertiserName: String?</code> is the name of the advertiser for a particular ad. This is only included for ad events.</p>"},{"location":"Analytics/#ad-id","title":"Ad ID","text":"<p>The <code>adId: String?</code> is the ad ID if an event is associated with an ad.</p>"},{"location":"Analytics/#ad-view","title":"Ad View","text":"<p>The <code>adView: View?</code> is the view the ad is rendered in. This is only included for <code>OpenedAd</code> and <code>FinishedAd</code> events.</p>"},{"location":"Analytics/#ad-type","title":"Ad Type","text":"<p>The <code>adType: String?</code> is the type of component on which the ad is displayed. This can have values <code>stories</code> or <code>clips</code>.</p>"},{"location":"Analytics/#ad-format","title":"Ad Format","text":"<p>The <code>adFormat: String?</code> represents the format of the Ad that was displayed. Possible values can be <code>customNative</code>, <code>native</code>, <code>banner</code>.</p>"},{"location":"Analytics/#ad-placement","title":"Ad Placement","text":"<p>The <code>adPlacement: String?</code> represents the placement of the ad. It can be either <code>Between Stories</code>, <code>Between Pages</code> or <code>Between Clips</code>. This is only included for ad events.</p> <p>For Story ad events, <code>AdOpportunity</code> and <code>OpenedAd</code> use the same placement naming (<code>Between Stories</code> / <code>Between Pages</code>).</p>"},{"location":"Analytics/#ad-strategy","title":"Ad Strategy","text":"<p>The <code>adStrategy: String?</code> represents the strategy used to display the ads. It can have the following values: <code>Between Stories</code>, <code>Between Pages</code>, <code>Between Stories and Pages</code>, <code>Between Clips</code>.</p>"},{"location":"Analytics/#ad-response-identifier","title":"Ad Response Identifier","text":"<p>The <code>adResponseIdentifier: String?</code> represents the response identifier attached to the ad that was received from the ad provider. Used for debugging ad targeting.</p>"},{"location":"Analytics/#ad-index","title":"Ad Index","text":"<p>The <code>adIndex: Int?</code> represents the order of the Ad within the displayed Ads in a Story or Clip collection (1 for the first Ad, 2 for the second, etc.). This field is included in all ad-related events.</p>"},{"location":"Analytics/#player-session-id","title":"Player Session ID","text":"<p>The <code>playerSessionId: String?</code> is a unique identifier for the current root player session. This is included on <code>AdOpportunity</code>, <code>AdRequested</code>, <code>AdLoaded</code>, <code>AdFailedToLoad</code>, <code>AdPaid</code>, and <code>AdSessionSummary</code> events.</p>"},{"location":"Analytics/#ad-request-id","title":"Ad Request ID","text":"<p>The <code>adRequestId: String?</code> is a unique identifier that correlates an ad request chain. This is included on <code>AdRequested</code>, <code>AdLoaded</code>, <code>AdFailedToLoad</code>, and <code>AdPaid</code> events. Concrete native/banner fallback attempts share the same identifier.</p>"},{"location":"Analytics/#ad-slot-type","title":"Ad Slot Type","text":"<p>The <code>adSlotType: String?</code> indicates the type of ad slot for <code>AdOpportunity</code>, <code>AdRequested</code>, <code>AdLoaded</code>, <code>AdFailedToLoad</code>, and <code>AdPaid</code> events. Possible values are <code>fullScreen</code> and <code>bottomBanner</code>.</p>"},{"location":"Analytics/#ad-unit-id","title":"Ad Unit ID","text":"<p>The <code>adUnitId: String?</code> is the exact ad unit identifier when it is known. It is available on paid ad operational events and is retained on rendered ad interaction events such as <code>OpenedAd</code>, <code>DismissedAd</code>, quartile events, and <code>FinishedAd</code>.</p> <p>For Storyteller GAM and AdMob integrations, the SDK records the unit immediately before each concrete request attempt. For GAM VAST ads, it uses the final <code>iu</code> tag parameter after client overrides. Host-supplied custom modules can provide the value through <code>StorytellerAd.adUnitId</code>.</p> <p><code>adUnitId</code> is <code>nil</code> for <code>AdOpportunity</code> because provider and unit selection have not occurred yet, and for <code>AdSessionSummary</code> because the event aggregates multiple requests. It can also be <code>nil</code> for Storyteller First Party Ads, direct VAST ads, or custom modules that do not supply a unit.</p>"},{"location":"Analytics/#ad-source","title":"Ad Source","text":"<p>The <code>adSource: String?</code> is the high-level source for ad events. Supported values include <code>storyteller</code> for Storyteller First Party Ads, <code>gam</code>, <code>admob</code>, and custom source values provided by integrating-app modules via <code>StorytellerModule.adSource</code>. This is included on all ad-related events. For <code>AdOpportunity</code>, <code>adSource</code> is always <code>nil</code> because the event is emitted before ad module resolution. For other ad events, <code>adSource</code> may still be <code>nil</code> when the source cannot be resolved.</p>"},{"location":"Analytics/#ad-mediation-group-name","title":"Ad Mediation Group Name","text":"<p>The <code>adMediationGroupName: String?</code> is the mediation group name returned by the ad provider when available. This is included on <code>AdLoaded</code> events when available.</p>"},{"location":"Analytics/#ad-mediation-ab-test-name","title":"Ad Mediation Ab Test Name","text":"<p>The <code>adMediationAbTestName: String?</code> is the mediation A/B test name returned by the ad provider when available. This is included on <code>AdLoaded</code> events when available.</p>"},{"location":"Analytics/#ad-mediation-ab-test-variant","title":"Ad Mediation Ab Test Variant","text":"<p>The <code>adMediationAbTestVariant: String?</code> is the mediation A/B test variant returned by the ad provider when available. This is included on <code>AdLoaded</code> events when available.</p>"},{"location":"Analytics/#ad-adapter-latency-millis","title":"Ad Adapter Latency Millis","text":"<p>The <code>adAdapterLatencyMillis: Int?</code> is the elapsed time in milliseconds for the concrete ad request attempt that produced the result. When native/banner fallback occurs, load, failure, and paid events use the final attempt's latency. This is included on <code>AdLoaded</code>, <code>AdFailedToLoad</code>, and <code>AdPaid</code> events.</p>"},{"location":"Analytics/#ad-error-code","title":"Ad Error Code","text":"<p>The <code>adErrorCode: Int?</code> is the numeric error code reported by the ad provider. This is included on <code>AdFailedToLoad</code> events.</p>"},{"location":"Analytics/#ad-error-domain","title":"Ad Error Domain","text":"<p>The <code>adErrorDomain: String?</code> is the error domain reported by the ad provider. This is included on <code>AdFailedToLoad</code> events.</p>"},{"location":"Analytics/#ad-error-message","title":"Ad Error Message","text":"<p>The <code>adErrorMessage: String?</code> is the error description reported by the ad provider. This is included on <code>AdFailedToLoad</code> events.</p>"},{"location":"Analytics/#ad-value-micros","title":"Ad Value Micros","text":"<p>The <code>adValueMicros: Int?</code> is the paid value in micros (1,000,000 micros = 1 unit of currency). This is only included on <code>AdPaid</code> events.</p>"},{"location":"Analytics/#ad-currency-code","title":"Ad Currency Code","text":"<p>The <code>adCurrencyCode: String?</code> is the currency code for the paid value (ISO 4217). This is only included on <code>AdPaid</code> events.</p>"},{"location":"Analytics/#ad-value-precision","title":"Ad Value Precision","text":"<p>The <code>adValuePrecision: String?</code> indicates how precise the paid value is. Possible values are <code>UNKNOWN</code>, <code>ESTIMATED</code>, <code>PUBLISHER_PROVIDED</code>, and <code>PRECISE</code>. This is only included on <code>AdPaid</code> events.</p>"},{"location":"Analytics/#ad-session-opportunities-count","title":"Ad Session Opportunities Count","text":"<p>The <code>adSessionOpportunitiesCount: Int?</code> is the total number of ad opportunities tracked in the player session. This is only included on <code>AdSessionSummary</code> events.</p>"},{"location":"Analytics/#ad-session-requests-count","title":"Ad Session Requests Count","text":"<p>The <code>adSessionRequestsCount: Int?</code> is the total number of ad-module request attempts initiated in the player session. Native/banner fallback attempts are counted separately, and a module failure before provider load counts once, so this value can exceed the number of ad opportunities. This is only included on <code>AdSessionSummary</code> events.</p>"},{"location":"Analytics/#ad-session-loads-count","title":"Ad Session Loads Count","text":"<p>The <code>adSessionLoadsCount: Int?</code> is the total number of successful ad loads in the player session. This is only included on <code>AdSessionSummary</code> events.</p>"},{"location":"Analytics/#ad-session-failed-to-load-count","title":"Ad Session Failed To Load Count","text":"<p>The <code>adSessionFailedToLoadCount: Int?</code> is the total number of ad load failures in the player session. This is only included on <code>AdSessionSummary</code> events.</p>"},{"location":"Analytics/#ad-session-paid-count","title":"Ad Session Paid Count","text":"<p>The <code>adSessionPaidCount: Int?</code> is the total number of paid callbacks received in the player session. This is only included on <code>AdSessionSummary</code> events.</p>"},{"location":"Analytics/#ad-session-revenue-micros","title":"Ad Session Revenue Micros","text":"<p>The <code>adSessionRevenueMicros: Int?</code> is the aggregated session paid value in micros. This is only included on <code>AdSessionSummary</code> events when all paid callbacks in the session use one currency and include complete paid data.</p>"},{"location":"Analytics/#ad-session-currency-code","title":"Ad Session Currency Code","text":"<p>The <code>adSessionCurrencyCode: String?</code> is the ISO 4217 currency code associated with <code>adSessionRevenueMicros</code>. This is only included on <code>AdSessionSummary</code> events when all paid callbacks in the session use one currency and include complete paid data.</p>"},{"location":"Analytics/#is-initial-buffering","title":"Is Initial Buffering","text":"<p>The <code>isInitialBuffering: Boolean?</code> value is returned if the buffering happens at the start of playback for that page. This is only included for <code>BufferingStarted</code> and <code>BufferingEnded</code> events.</p>"},{"location":"Analytics/#time-since-buffering-began","title":"Time Since Buffering Began","text":"<p>The <code>timeSinceBufferingBegan: TimeInterval?</code> value is the duration the current buffering lasted for in milliseconds. This is only included for <code>BufferingEnded</code> events.</p>"},{"location":"Analytics/#categories","title":"Categories","text":"<p>The <code>categories: [String]?</code> value is the list of categories assigned to the story or clip for which the event occurred. This is included on <code>OpenedStory</code> and <code>TileVisible</code> events, and may be included on other story, clip, ad, card, search, or sheet events when category context is available.</p>"},{"location":"Analytics/#poll-answer","title":"Poll Answer","text":"<p>The <code>pollAnswerId: String?</code> is the ID of the answer the user selected when voting. This is only included on <code>VotedPoll</code> events.</p>"},{"location":"Analytics/#trivia-quiz-id","title":"Trivia Quiz ID","text":"<p>The <code>triviaQuizId: String?</code> is the ID of the trivia quiz that was completed or answered. This is only included on <code>TriviaQuizQuestionAnswered</code> and <code>TriviaQuizCompleted</code> events.</p>"},{"location":"Analytics/#trivia-quiz-title","title":"Trivia Quiz Title","text":"<p>The <code>triviaQuizTitle: String?</code> is the title of the trivia quiz that was completed or answered. This is only included on <code>TriviaQuizQuestionAnswered</code> and <code>TriviaQuizCompleted</code> events.</p>"},{"location":"Analytics/#trivia-quiz-score","title":"Trivia Quiz Score","text":"<p>The <code>triviaQuizScore: Int?</code> value is the score of the trivia quiz that was completed. This is only included on <code>TriviaQuizCompleted</code> events.</p>"},{"location":"Analytics/#trivia-quiz-question-id","title":"Trivia Quiz Question ID","text":"<p>The <code>triviaQuizQuestionId: String?</code> is the ID of the trivia quiz question which was answered. This is only included on <code>TriviaQuizQuestionAnswered</code> events.</p>"},{"location":"Analytics/#trivia-quiz-answer-id","title":"Trivia Quiz Answer ID","text":"<p>The <code>triviaQuizAnswerId: String?</code> is the ID of the selected trivia quiz answer. This is only included on <code>TriviaQuizQuestionAnswered</code> events.</p>"},{"location":"Analytics/#clip-id","title":"Clip ID","text":"<p>The <code>clipId: String?</code> is the ID of the clip for which the event occurred.</p>"},{"location":"Analytics/#clip-title","title":"Clip Title","text":"<p>The <code>clipTitle: String?</code> is the title of the clip for which the event occurred.</p>"},{"location":"Analytics/#clip-index","title":"Clip Index","text":"<p>The <code>clipIndex: Int?</code> is the index of the clip in the row or grid at the point it was selected or the index of the clip in the player inside the original row or grid. For Ad events, clip index refers to the index of the clip before the Ad.</p>"},{"location":"Analytics/#tile-index","title":"Tile Index","text":"<p>The <code>tileIndex: Int?</code> is the index of the Story or Clip tile in the row or grid at the point it became visible. This is only included on <code>TileVisible</code> events.</p> <p>Note: This value is 1-based.</p>"},{"location":"Analytics/#clip-collection-count","title":"Clip Collection Count","text":"<p>The <code>clipCollectionCount: Int?</code> is the total number of clips in the collection being viewed.</p>"},{"location":"Analytics/#clip-feed-type","title":"Clip Feed Type","text":"<p>The <code>clipFeedType: String?</code> indicates the type of clip feed being viewed. Possible values are <code>default</code>, <code>forYou</code>, and <code>following</code>.</p>"},{"location":"Analytics/#clips-viewed","title":"Clips Viewed","text":"<p>The <code>clipsViewed: Int?</code> value is the total number of clips a user has viewed since the most recent OpenedClip event with an Opened Reason of <code>clipListTap</code>, <code>pageActionButton</code>, <code>card</code>, <code>clipActionButton</code> or <code>deepLink</code>. This count should be reset after any DismissedClip events.</p>"},{"location":"Analytics/#loops-viewed","title":"Loops Viewed","text":"<p>The <code>loopsViewed: Int?</code> is for DismissedClip, the total number of loops (plays of an individual clip) a user has viewed since the most recent OpenedClip event with an Opened Reason of <code>clipListTap</code>, <code>pageActionButton</code>, <code>card</code>, <code>clipActionButton</code> or <code>deepLink</code>.</p> <p>This count should be reset after any DismissedClip events. For FinishedClip, the total number of loops.</p>"},{"location":"Analytics/#is-live","title":"Is Live","text":"<p>The <code>isLive: Bool?</code> indicates whether a Clip, Story, or Card is currently being broadcast in real-time and is therefore \"Live\".</p>"},{"location":"Analytics/#airplay-enabled","title":"Airplay Enabled","text":"<p>The <code>airplayEnabled: Bool?</code> indicates whether the device is currently using AirPlay for audio/video output when the event occurred.</p>"},{"location":"Analytics/#clip-has-action","title":"Clip Has Action","text":"<p>The <code>clipHasAction: Bool?</code> value is whether the clip associated with the event contains a primary action.</p>"},{"location":"Analytics/#clip-action-text","title":"Clip Action Text","text":"<p>The <code>clipActionText: String?</code> is the text call to action if the clip associated with the event has a primary action link.</p>"},{"location":"Analytics/#clip-action-url","title":"Clip Action URL","text":"<p>The <code>clipActionUrl: String?</code> is the URL linked to from the primary action if a clip associated with the event has a primary action.</p>"},{"location":"Analytics/#clip-action-type","title":"Clip Action Type","text":"<p>The <code>clipActionType: String?</code> is the type of the primary action on a clip associated with the event. If there is no action button then the value is nil.</p>"},{"location":"Analytics/#collection","title":"Collection","text":"<p>The <code>collection</code> is the ID of the collection if a story or clip is being played from a collection.</p>"},{"location":"Analytics/#collection-title","title":"Collection Title","text":"<p>The <code>collectionTitle: String?</code> is the title of the collection if clip is being played from a collection.</p>"},{"location":"Analytics/#container-title","title":"Container Title","text":"<p>The <code>containerTitle</code> is the title of the collection if a story or clip is being played from a collection.</p>"},{"location":"Analytics/#category-details","title":"Category Details","text":"<p>The <code>categoryDetails</code> is a list of Category Detail objects. The details are the name, ID, type and placement of the category.</p>"},{"location":"Analytics/#category-name","title":"Category Name","text":"<p>The <code>categoryName</code> of the category being navigated to or dismissed.</p>"},{"location":"Analytics/#category-id","title":"Category ID","text":"<p>The <code>categoryId</code> of the category being navigated to or dismissed.</p>"},{"location":"Analytics/#current-category","title":"Current Category","text":"<p>The <code>currentCategory</code> is the category for the row that is currently being interacted with. The information provided from this is the category title, ID and placement.</p> <p>This is only included on story and ad events.</p>"},{"location":"Analytics/#captions-enabled","title":"Captions Enabled","text":"<p>The <code>captionsEnabled</code> property indicates whether captions are currently enabled for the Clips Player. This is included in all Clip Analytics Events and represents the state of captions at the time the event occurred.</p>"},{"location":"Analytics/#start-position","title":"Start Position","text":"<p>The <code>startPosition: Int?</code> represents the playback position, in milliseconds, when the user started scrubbing the clip. This is only included for <code>ScrubbedClip</code> events.</p>"},{"location":"Analytics/#end-position","title":"End Position","text":"<p>The <code>endPosition: Int?</code> represents the playback position, in milliseconds, when the user stopped scrubbing the clip. This is only included for <code>ScrubbedClip</code> events.</p>"},{"location":"Analytics/#eyebrow","title":"Eyebrow","text":"<p>The <code>eyebrow: String?</code> is a text field displayed on stories and clips, typically shown above the main title as a subtitle or descriptor.</p>"},{"location":"Analytics/#scrub-direction","title":"Scrub Direction","text":"<p>The <code>scrubDirection: String?</code> indicates whether the user scrubbed to a position forward or backward on a Clip. Possible values are <code>forward</code> and <code>backward</code>. This is only included for <code>ScrubbedClip</code> events.</p>"},{"location":"Analytics/#completion-type","title":"Completion Type","text":"<p>The <code>completionType: String?</code> indicates how a user finished watching a Clip. Possible values are <code>natural</code> and <code>scrubbed</code>. This is included for <code>FinishedClip</code> and <code>ClipCompletedLoop</code> events.</p>"},{"location":"Analytics/#sheet-id","title":"Sheet ID","text":"<p>The ID of the Sheet for which the event occurred.</p>"},{"location":"Analytics/#sheet-size","title":"Sheet Size","text":"<p>The height of the Sheet for which the event occurred. Possible values are <code>50</code>, <code>75</code>, and <code>100</code> (representing % of screen height).</p>"},{"location":"Analytics/#sheet-title","title":"Sheet Title","text":"<p>The title of the Sheet for which the event occurred.</p>"},{"location":"Analytics/#search-from","title":"Search From","text":"<p>The <code>searchFrom</code> indicates whether the Search for which the event is recorded was opened from Clips or Stories.</p>"},{"location":"Analytics/#is-suggestion","title":"Is Suggestion","text":"<p>The <code>isSuggestion</code> indicates whether the Search for which the event is recorded used a suggested Search.</p> <p>Note: When filters are opened (event), the Is Suggestion value should reflect what was used for the initial search performed before opening filters.</p>"},{"location":"Analytics/#initial-input","title":"Initial Input","text":"<p>The <code>initialInput</code> this property tracks the input at the moment the suggestion was used.</p>"},{"location":"Analytics/#search-filter","title":"Search Filter","text":"<p>The <code>searchFilter</code> property includes the content type and date posted used by search filter.</p>"},{"location":"Analytics/#search-sort","title":"Search Sort","text":"<p>The <code>searchSort</code> field indicates method by which the relevant Story / Clip's search results were sorted.</p>"},{"location":"Analytics/#search-term","title":"Search Term","text":"<p>The <code>searchTerm</code> by which the Clips / Stories were searched, either entered in the search bar by the user or selected / filled from search suggestions.</p>"},{"location":"Analytics/#action-text","title":"Action Text","text":"<p>The <code>actionText: String?</code> property is used for general action text and can apply to both page actions and clip actions depending on the placement context.</p>"},{"location":"Analytics/#action-type","title":"Action Type","text":"<p>The <code>actionType: String?</code> property is used for general action types, and can apply to both page actions and clip actions depending on the placement context.</p>"},{"location":"Analytics/#action-url","title":"Action URL","text":"<p>The <code>actionUrl: String?</code> is the URL for the action link. This property is used for general action URLs in events and can apply to both page actions and clip actions depending on the placement context.</p>"},{"location":"Analytics/#action-class","title":"Action Class","text":"<p>The <code>actionClass: String?</code> identifies whether the action button that was tapped is a primary or secondary action. Possible values are <code>primary</code> and <code>secondary</code>. This is only included for <code>ClipActionButtonTapped</code> events.</p>"},{"location":"Analytics/#action-index","title":"Action Index","text":"<p>The <code>actionIndex: Int?</code> is the 1-based index of the secondary action that was tapped. This is only included for <code>ClipActionButtonTapped</code> events when a secondary action is tapped.</p>"},{"location":"Analytics/#tapped-clip-action-text","title":"Tapped Clip Action Text","text":"<p>The <code>tappedClipActionText: String?</code> is the text of the specific action button (primary or secondary) that was tapped. This is only included for <code>ClipActionButtonTapped</code> events.</p>"},{"location":"Analytics/#tapped-clip-action-url","title":"Tapped Clip Action URL","text":"<p>The <code>tappedClipActionUrl: String?</code> is the URL of the specific action button (primary or secondary) that was tapped. This is only included for <code>ClipActionButtonTapped</code> events.</p>"},{"location":"Analytics/#tapped-clip-action-type","title":"Tapped Clip Action Type","text":"<p>The <code>tappedClipActionType: String?</code> is the type of the specific action button (primary or secondary) that was tapped. This is only included for <code>ClipActionButtonTapped</code> events.</p>"},{"location":"Analytics/#has-secondary-actions","title":"Has Secondary Actions","text":"<p>The <code>clipHasSecondaryActions: Bool?</code> indicates whether the clip has any secondary actions. This is included in all Clip Analytics Events.</p>"},{"location":"Analytics/#secondary-actions-text","title":"Secondary Actions Text","text":"<p>The <code>clipSecondaryActionsText: [String]?</code> is an array of all the text CTAs on secondary actions for the clip. This is included in all Clip Analytics Events when the clip has secondary actions.</p>"},{"location":"Analytics/#secondary-action-urls","title":"Secondary Action URLs","text":"<p>The <code>clipSecondaryActionUrls: [String]?</code> is an array of all the URLs on secondary actions for the clip. This is included in all Clip Analytics Events when the clip has secondary actions.</p>"},{"location":"Analytics/#secondary-action-types","title":"Secondary Action Types","text":"<p>The <code>clipSecondaryActionTypes: [String]?</code> is an array of all the types of secondary actions for the clip. This is included in all Clip Analytics Events when the clip has secondary actions.</p>"},{"location":"Analytics/#card-id","title":"Card ID","text":"<p>The <code>cardId: String?</code> is the ID of the Card for which the event occurred. This is included for <code>CardTapped</code> event.</p>"},{"location":"Analytics/#card-action-type","title":"Card Action Type","text":"<p>The <code>cardActionType: String?</code> is the type of the action on the Card. This is included for <code>CardTapped</code> event.</p>"},{"location":"Analytics/#card-action-url","title":"Card Action URL","text":"<p>The <code>cardActionUrl: String?</code> is the URL for the link if the Card has an action. This is included for <code>CardTapped</code> event.</p>"},{"location":"Analytics/#card-aspect-ratio","title":"Card Aspect Ratio","text":"<p>The <code>cardAspectRatio: String?</code> is the aspect ratio of the Card. This is included for <code>CardTapped</code> event.</p>"},{"location":"Analytics/#card-background-type","title":"Card Background Type","text":"<p>The <code>cardBackgroundType: String?</code> is the type of background on the Card. This can have the value <code>image</code> or <code>video</code>. This is included for <code>CardTapped</code> event.</p>"},{"location":"Analytics/#card-collection-id","title":"Card Collection ID","text":"<p>The <code>cardCollectionId: String?</code> is the ID of the collection the Card belongs to. This is included for <code>CardTapped</code> event.</p>"},{"location":"Analytics/#card-index","title":"Card Index","text":"<p>The <code>cardIndex: Int?</code> is the index of the Card in the collection.</p> <p>Note: This value is 1-based.</p>"},{"location":"Analytics/#card-subtitle","title":"Card Subtitle","text":"<p>The <code>cardSubtitle: String?</code> is the subtitle text displayed on the Card. This is included for <code>CardTapped</code> event.</p>"},{"location":"Analytics/#card-title","title":"Card Title","text":"<p>The <code>cardTitle: String?</code> is the title text displayed on the Card. This is included for <code>CardTapped</code> event.</p>"},{"location":"Analytics/#has-button","title":"Has Button","text":"<p>The <code>hasButton: Bool?</code> indicates whether the Card displayed a button. This is included for <code>CardViewed</code> and <code>CardTapped</code> events.</p>"},{"location":"Analytics/#has-action","title":"Has Action","text":"<p>The SDK does not emit a <code>hasAction</code> property. Use <code>pageHasAction</code> (story/page-based ad events) or <code>clipHasAction</code> (clip-based ad events) depending on the ad placement context.</p>"},{"location":"Analytics/#followable-category-limit-dialogue","title":"Followable Category Limit Dialogue","text":"<p>The <code>followableCategoryLimitDialogue: String?</code> is the text displayed in the Followable Category Limit dialogue box. This is included for <code>FollowableCategoryLimitShown</code>, <code>FollowableCategoryLimitActionButtonTapped</code>, and <code>FollowableCategoryLimitDismissed</code> events.</p>"},{"location":"Analytics/#followable-category-limit-action-text","title":"Followable Category Limit Action Text","text":"<p>The <code>followableCategoryLimitActionText: String?</code> is the text displayed on the customisable action button in the Followable Category Limit dialogue box. This is included for <code>FollowableCategoryLimitShown</code>, <code>FollowableCategoryLimitActionButtonTapped</code>, and <code>FollowableCategoryLimitDismissed</code> events.</p>"},{"location":"Analytics/#followable-category-limit-action-url","title":"Followable Category Limit Action URL","text":"<p>The <code>followableCategoryLimitActionUrl: String?</code> is the URL associated with the customisable action button in the Followable Category Limit dialogue box. This is included for <code>FollowableCategoryLimitShown</code>, <code>FollowableCategoryLimitActionButtonTapped</code>, and <code>FollowableCategoryLimitDismissed</code> events.</p>"},{"location":"Analytics/#is-muted","title":"Is Muted","text":"<p>The <code>isMuted: Bool?</code> indicates whether the audio is currently muted when the event occurred. This property is included in all Story, Clip, Ad, and video Card analytics events when the mute state is known and represents the mute state at the time the event was tracked. Permanently muted video Cards and video Cards marked as having no audio report <code>true</code>; image and non-video Card events report <code>nil</code>.</p>"},{"location":"Analytics/#metadata","title":"Metadata","text":"<p>The <code>metadata: [String: String]?</code> contains custom metadata associated with the content for which the event occurred. For Clip events, this contains the metadata from the associated clip. For Story events, this contains the metadata from the associated page. This property is included in all Story and Clip analytics events.</p>"},{"location":"Analytics/#context","title":"Context","text":"<p><code>context: StorytellerAnalyticsContext?</code> contains host-defined attribution data. <code>StorytellerAnalyticsContext</code> is a type alias for <code>[String: String]</code>; the SDK does not prescribe the keys.</p> <p>You can provide context through:</p> <ul> <li><code>StorytellerStoriesListConfiguration</code></li> <li><code>StorytellerClipsListConfiguration</code></li> <li><code>StorytellerClipCollectionConfiguration</code></li> <li><code>StorytellerCardConfiguration</code></li> <li><code>StorytellerHomeConfiguration</code></li> </ul> <p>The SDK carries that context into <code>StorytellerUserActivityData.context</code> for events it can attribute to the configured surface and content opened from it. Consume it in <code>StorytellerDelegate.onUserActivityOccurred</code>:</p> <pre><code>import StorytellerSDK\n\nlet analyticsContext: StorytellerAnalyticsContext = [\n    \"location\": \"home\",\n    \"campaign\": \"summer-league\"\n]\n\nlet clipsConfiguration = StorytellerClipsListConfiguration(\n    collectionId: \"top-plays\",\n    context: analyticsContext\n)\n\nfinal class AnalyticsDelegate: StorytellerDelegate {\n    func onUserActivityOccurred(\n        type: StorytellerUserActivity.EventType,\n        data: StorytellerUserActivityData\n    ) {\n        guard let location = data.context?[\"location\"] else { return }\n        print(\"Storyteller event \\(type) came from \\(location)\")\n    }\n}\n</code></pre> <p>The callback is delivered only when <code>enableUserActivityTracking</code> is enabled. Ad events are additionally gated by <code>enableAdTracking</code>.</p> <p>The Showcase app demonstrates an optional app-defined key namespace and consumption in <code>StorytellerTrackingDelegate</code>.</p>"},{"location":"AnalyticsIntegration/","title":"Integrate Analytics","text":"<p>Storyteller delivers user activity events to your app through <code>StorytellerDelegate.onUserActivityOccurred(type:data:)</code>. This guide shows how to retain the delegate, select the tracking behavior during initialization, forward events to your analytics layer, and add host-defined attribution context.</p> <p>Use the Analytics Event Reference after setup to choose the event types and payload fields your analytics implementation needs.</p>"},{"location":"AnalyticsIntegration/#retain-a-delegate-and-forward-events","title":"Retain a Delegate and Forward Events","text":"<p><code>Storyteller.shared.delegate</code> is weak. Keep the delegate in app-owned state for as long as you need callbacks, and assign the same object that handles your other global Storyteller callbacks. If another part of the app later replaces <code>Storyteller.shared.delegate</code>, the original object stops receiving events.</p> <p>The following complete example forwards the serialized event key (<code>type.rawValue</code>), selected content identifiers, and any analytics context into a provider-independent host analytics layer. Replace <code>ConsoleAnalytics</code> with your own analytics adapter.</p> <pre><code>import StorytellerSDK\n\nstruct HostAnalyticsEvent {\n    let name: String\n    let storyId: String?\n    let clipId: String?\n    let context: StorytellerAnalyticsContext?\n}\n\nprotocol HostAnalyticsTracking: AnyObject {\n    func track(_ event: HostAnalyticsEvent)\n}\n\nfinal class ConsoleAnalytics: HostAnalyticsTracking {\n    func track(_ event: HostAnalyticsEvent) {\n        print(\n            \"Storyteller event: \\(event.name), \"\n                + \"story: \\(event.storyId ?? \"none\"), \"\n                + \"clip: \\(event.clipId ?? \"none\"), \"\n                + \"context: \\(event.context ?? [:])\"\n        )\n    }\n}\n\nfinal class StorytellerAnalyticsDelegate: StorytellerDelegate {\n    private let analytics: any HostAnalyticsTracking\n\n    init(analytics: any HostAnalyticsTracking) {\n        self.analytics = analytics\n    }\n\n    func onUserActivityOccurred(\n        type: StorytellerUserActivity.EventType,\n        data: StorytellerUserActivityData\n    ) {\n        analytics.track(\n            HostAnalyticsEvent(\n                name: type.rawValue,\n                storyId: data.storyId,\n                clipId: data.clipId,\n                context: data.context\n            )\n        )\n    }\n}\n\n@MainActor\nfinal class StorytellerIntegration {\n    private let storytellerDelegate: StorytellerAnalyticsDelegate\n\n    init(analytics: any HostAnalyticsTracking) {\n        let delegate = StorytellerAnalyticsDelegate(analytics: analytics)\n        storytellerDelegate = delegate\n        Storyteller.shared.delegate = delegate\n    }\n\n    func initialize(\n        apiKey: String,\n        userId: String,\n        trackingOptions: StorytellerEventTrackingOptions\n    ) async throws {\n        try await Storyteller.shared.initialize(\n            apiKey: apiKey,\n            userInput: StorytellerUserInput(externalId: userId),\n            eventTrackingOptions: trackingOptions\n        )\n    }\n}\n\n@MainActor\nfinal class AppServices {\n    private let storyteller = StorytellerIntegration(\n        analytics: ConsoleAnalytics()\n    )\n\n    func start() async throws {\n        // Use .enableAll only when it matches your app's consent policy.\n        try await storyteller.initialize(\n            apiKey: \"your-api-key\",\n            userId: \"your-user-id\",\n            trackingOptions: .enableAll\n        )\n    }\n}\n</code></pre> <p><code>StorytellerUserActivityData</code> is an event-specific payload, so most properties are optional. Forward only the fields your analytics contract needs, using the event reference to determine which fields apply to each event. The Showcase app demonstrates provider-specific mapping in <code>StorytellerTrackingDelegate</code>.</p>"},{"location":"AnalyticsIntegration/#choose-tracking-options-during-initialization","title":"Choose Tracking Options During Initialization","text":"<p>Pass <code>StorytellerEventTrackingOptions</code> when you call <code>initialize(...)</code>. The value is fixed for that initialization; reinitialize the SDK to apply a later consent change.</p> <p>For host analytics delivery, these options have distinct effects:</p> Option Effect on <code>onUserActivityOccurred</code> <code>enableUserActivityTracking</code> Must be enabled for the integrating app to receive user activity events. <code>enableAdTracking</code> Must also be enabled for Ad-related user activity events. It does not control whether a host-supplied Ad loading callback is requested. <code>enableStorytellerTracking</code> Controls Storyteller's own analytics collection; <code>enableUserActivityTracking</code> remains the host callback gate. <code>enableFullVideoAnalytics</code> When disabled, callbacks still arrive, but content identifiers and titles listed in Privacy and Tracking are removed from their payloads. <p>The other options affect personalization, viewing state, and functional behavior. Choose the complete configuration from your app's consent requirements; see Privacy and Tracking before changing defaults.</p>"},{"location":"AnalyticsIntegration/#add-analytics-context","title":"Add Analytics Context","text":"<p><code>StorytellerAnalyticsContext</code> is a type alias for <code>[String: String]</code>. The SDK does not prescribe its keys. Add a context dictionary to the configuration for the surface or presentation you want to attribute:</p> <ul> <li><code>StorytellerStoriesListConfiguration</code></li> <li><code>StorytellerClipsListConfiguration</code></li> <li><code>StorytellerClipCollectionConfiguration</code>, including UIKit and SwiftUI Embedded Clips</li> <li><code>StorytellerCardConfiguration</code></li> <li><code>StorytellerHomeConfiguration</code></li> </ul> <p>For example, an Embedded Clips configuration can identify both its screen and placement:</p> <pre><code>let clipsConfiguration = StorytellerClipCollectionConfiguration(\n    collectionId: \"top-plays\",\n    context: [\n        \"screen\": \"home\",\n        \"placement\": \"primary-clips-feed\"\n    ]\n)\n</code></pre> <p>The SDK carries the dictionary into <code>StorytellerUserActivityData.context</code> when an event can be attributed to that configured surface or to content opened from it. The property remains optional: events without an attributable configured surface do not receive a context value. Set context before loading or opening the content whose events you want to attribute.</p> <p>See Context in the Analytics Event Reference for the complete attribution contract and another consumption example.</p>"},{"location":"AnalyticsIntegration/#verify-the-integration","title":"Verify the Integration","text":"<p>After initialization succeeds:</p> <ol> <li>Load known published content using a configuration with a distinctive context value.</li> <li>Open or interact with that content to produce a documented event, such as <code>openedStory</code> or <code>openedClip</code>.</li> <li>Confirm your analytics adapter receives the expected <code>type.rawValue</code> and payload.</li> <li>Confirm <code>data.context</code> contains the value supplied by the originating configuration when that event is attributable to the surface.</li> </ol> <p>Initialization success alone does not exercise this callback path or generate a host user activity callback. Trigger a supported content interaction when testing the integration.</p>"},{"location":"AnalyticsIntegration/#troubleshoot-missing-events-or-context","title":"Troubleshoot Missing Events or Context","text":"<p>If no event arrives:</p> <ul> <li>Confirm the app still strongly retains its delegate and that <code>Storyteller.shared.delegate</code> has not been replaced.</li> <li>Confirm <code>enableUserActivityTracking</code> was enabled during the current SDK initialization.</li> <li>For an Ad event, also confirm <code>enableAdTracking</code> is enabled and that the corresponding Ad lifecycle point was actually reached.</li> <li>Confirm the documented interaction occurred; loading and initialization callbacks are separate from user activity delivery.</li> </ul> <p>If the callback arrives but data is missing:</p> <ul> <li>Check <code>enableFullVideoAnalytics</code> before treating absent Story, Page, Clip, or Card identifiers and titles as an SDK fault.</li> <li>For missing context, confirm the active configuration supplied it before the content was loaded or opened and that the event can be attributed to that surface.</li> <li>Treat every event payload as event-specific; unrelated fields are expected to be <code>nil</code>.</li> </ul> <p>Use Callbacks or Analytics Events Do Not Arrive to separate analytics delivery from component loading, app navigation, and Ad loading callbacks.</p>"},{"location":"AnalyticsIntegration/#continue-with-the-event-reference","title":"Continue with the Event Reference","text":"<p>Use the Analytics Event Reference for all public event keys, the common fields for each feature, event-specific fields, and enum value definitions.</p>"},{"location":"Brightcove/","title":"Storyteller Brightcove Integration","text":"<p>This package is a utility to record player events from an <code>AVPlayer</code> and feed them into Brightcove analytics.</p>"},{"location":"Brightcove/#setup","title":"Setup","text":"<p>Add Storyteller Brightcove Collector using SPM:</p> <ol> <li>Add the package dependency: <code>https://github.com/getstoryteller/storyteller-brightcove-collector-swift</code></li> <li>Add the <code>StorytellerBrightcoveIntegration</code> target to your app target</li> </ol> <p>To use the module, StorytellerSDK needs to be set to version <code>11.0.0</code> or higher.</p> <p>In the file where you setup the Storyteller SDK, import the added package, set up the module and set it on the <code>Storyteller</code> class:</p> <pre><code>import StorytellerBrightcoveIntegration\n\nfunc initializeStoryteller() {\n    let brightcoveConfiguration = StorytellerBrightcoveModuleConfiguration(\n        account: \"&lt;account ID&gt;\",\n        playerName: \"Video Player\", // optional\n        source: \"&lt;source&gt;\",\n        destination: \"&lt;destination&gt;\"\n    )\n\n    Storyteller.shared.modules.append(StorytellerBrightcoveModule(configuration: brightcoveConfiguration))\n\n    // other Storyteller initialization code\n}\n</code></pre> <p>See the Brightcove module setup example in the Showcase app: <code>AppDelegate.setupStoryteller</code>.</p>"},{"location":"Cards/","title":"Storyteller Cards","text":"<p>Storyteller Cards are flexible, themeable components designed to promote content or direct users to key sections within your app. They can display a background image or video, along with an optional title, subtitle, and button. Tapping on a Card can trigger various actions, such as opening a specific Story, a Story Category, a Clip, a Clip Collection or any other action defined in the CMS. The server or personalization engine can choose which Cards to return for a given user.</p> <p>Cards currently support <code>1:1</code>, <code>2:3</code>, <code>3:4</code>, <code>4:5</code>, <code>9:16</code>, <code>16:9</code>, and <code>4:1</code> aspect ratios from CMS payloads.</p>"},{"location":"Cards/#usage","title":"Usage","text":"<p>You can integrate Storyteller Cards into your app using either SwiftUI or UIKit.</p>"},{"location":"Cards/#swiftui","title":"SwiftUI","text":"<p>For SwiftUI, use the <code>StorytellerCard</code> view component.</p> <ol> <li>Configuration: Create a <code>StorytellerCardConfiguration</code> object, specifying the <code>collectionId</code> for the Card collection you want to display. You can also provide optional <code>context</code> data for analytics attribution. When configured, <code>context</code> will be included in all analytics events when users interact with the Card. See Analytics for more details.</li> <li>Model: Initialize a <code>StorytellerCardModel</code> (which conforms to <code>ObservableObject</code>) with the configuration.</li> <li>View: Create the <code>StorytellerCard</code> view, passing in the <code>StorytellerCardModel</code> instance.</li> <li>Actions (Optional): Provide an optional callback closure to the <code>StorytellerCard</code> initializer to handle events like <code>onDataLoadComplete</code>. This allows you to react to data loading success or failure (e.g., by hiding the component).</li> </ol> <pre><code>import SwiftUI\nimport StorytellerSDK\n\n@available(iOS 14.0, *)\nstruct SwiftUIView: View {\n    @StateObject private var cardModel = StorytellerCardModel(\n        configuration: StorytellerCardConfiguration(\n            collectionId: \"card-collection-id\",\n            context: [\"source\": \"hero-banner\"]\n        )\n    )\n\n    var body: some View {\n        VStack {\n            Text(\"Storyteller Card Section\")\n            StorytellerCard(model: cardModel) { action in\n                switch action {\n                case .onDataLoadComplete(let result):\n                    switch result {\n                    case .success:\n                        print(\"Card data loaded successfully!\")\n                    case .failure(let error):\n                        print(\"Card data failed to load: \\(error.localizedDescription)\")\n                    }\n                @unknown default:\n                    break\n                }\n            }\n\n            Button(\"Reload Card\") {\n                cardModel.reload()\n            }\n        }\n        .padding()\n    }\n}\n</code></pre> <p>See the Showcase <code>StorytellerCard</code> integration in <code>StorytellerItemView</code>.</p> <p>See the Showcase card callback handling in <code>StorytellerItemView.cardAction</code>.</p>"},{"location":"Cards/#uikit","title":"UIKit","text":"<p>For UIKit, use the <code>StorytellerCardView</code> class, which subclasses <code>UIView</code>.</p> <ol> <li>Configuration: Create a <code>StorytellerCardConfiguration</code> with the desired <code>collectionId</code>. You can also provide optional <code>context</code> data for analytics. When configured, <code>context</code> will be included in all analytics events when users interact with the Card. See Analytics for more details.</li> <li>View Initialization: Instantiate <code>StorytellerCardView</code> using the configuration.</li> <li>Delegate (Optional): Assign an object conforming to <code>StorytellerCardViewDelegate</code> to the view's <code>delegate</code> property to receive callbacks like <code>onDataLoadComplete</code>.</li> <li>Add to View Hierarchy: Add the <code>StorytellerCardView</code> instance as a subview.</li> </ol> <pre><code>class CardView: UIView, StorytellerCardViewDelegate {\n\n    private var storytellerCardView: StorytellerCardView?\n\n    // ...\n\n    func configure(with collectionId: String, delegate: StorytellerCardViewDelegate?) {\n        let configuration = StorytellerCardConfiguration(\n            collectionId: collectionId,\n            context: [\n                \"source\": \"hero-banner\",\n            ]\n        )\n        let cardView = StorytellerCardView(configuration: configuration)\n        cardView.delegate = delegate\n\n        addSubview(cardView)\n        // Add constraints\n\n        self.storytellerCardView = cardView\n    }\n\n    func reloadCard() {\n        storytellerCardView?.reload()\n    }\n\n    nonisolated func onDataLoadComplete(result: Result&lt;Void, Error&gt;) {\n        // Respond to the load result as needed.\n    }\n}\n</code></pre> <p>For UIKit integration patterns using table view cells, see the CocoaPods sample in <code>MultipleListsDataSource</code>.</p> <p>We also provide cells to include inside collection and table views - <code>StorytellerCardCollectionViewCell</code> and <code>StorytellerCardTableViewCell</code>. They also follow the above pattern to setup.</p>"},{"location":"Cards/#reloading","title":"Reloading","text":"<p>Both <code>StorytellerCardModel</code> (for SwiftUI) and the UIKit flavours provide a <code>reload()</code> method. Call this method to manually trigger a refresh of the Card data from the server.</p>"},{"location":"Cards/#viewedtapped-ordering","title":"Viewed/Tapped Ordering","text":"<p>In the CMS you can make Card collections be ordered based on viewed or tapped status, so that once a Card is viewed/tapped, the next Card from the collection will be shown to the user. This will enable users to always see fresh content.</p>"},{"location":"Cards/#video-card-audio","title":"Video Card Audio","text":"<p>Video Cards start from the Cards audio behavior in tenant settings. For the usual muted-toggle setup, the active video Card shows a mute/unmute control and starts muted. The selected Cards audio state is shared across the current Cards surface, so if a user unmutes one video Card, newly active video Cards stay unmuted until the user mutes again or an audio interruption occurs.</p> <p>Only the active video Card can be audible. Inactive video Cards stay muted, and image Cards do not show an audio control. If the API marks a video Card as having no audio, the active Card does not show an audio control and remains muted.</p> <p>Cards audio behavior is controlled by the CMS/API settings theme under <code>theme.behavior.cards</code>:</p> <ul> <li><code>showMuteToggle</code>: set <code>true</code> to show the toggle for active audio-capable video Cards. False, nil, or missing keeps Cards muted with no icon.</li> <li><code>persistMuteState</code>: set <code>true</code> to persist the user's Cards mute choice. False, nil, or missing keeps Cards state session-scoped.</li> <li><code>defaultMuteState</code>: uses <code>soundOff</code>, <code>soundOn</code>, or <code>respectDeviceSilentToggle</code>. Missing, nil, or unrecognized values default to <code>soundOff</code> for Cards.</li> </ul>"},{"location":"Cards/#theming","title":"Theming","text":"<p>Card appearance and behavior are primarily configured directly within the Storyteller CMS for each Card Collection. The following properties can be configured in the CMS and influence the Card's presentation and behaviour:</p>"},{"location":"Cards/#button-behavior","title":"Button Behavior","text":"<ul> <li>Button positioning: Buttons are optional visual elements that follow the <code>textOverContent</code> property:</li> <li>When <code>textOverContent = true</code>: Button appears on the card (overlaying the content), positioned below the title/subtitle</li> <li>When <code>textOverContent = false</code>: Button appears below the card (below the title/subtitle section)</li> <li>Button functionality: Buttons do not change the tappability of Cards - the entire card remains tappable and executes the same action as the button when tapped</li> <li>Button text: The button text is defined in the Card data, not the theme</li> <li> <p>Button content inset: Buttons apply a built-in horizontal content inset when the button text is edge-aligned (<code>start</code> or <code>end</code>), using the resolved <code>style.padding</code> value so the label does not sit flush against the border.</p> </li> <li> <p><code>style.textLengthMode</code> (default: <code>truncate</code>): How text that exceeds the available space is handled.</p> </li> <li><code>truncate</code>: Display text at the specified size; truncate with an ellipsis (...) if it doesn't fit.</li> <li><code>resize</code>: Start at the specified text size and reduce the font size until the text fits (up to two lines for heading and subheading).</li> <li><code>style.textAlignment</code> (default: <code>start</code>): Horizontal alignment of the heading and subheading. Can be <code>start</code>, <code>center</code>, or <code>end</code>.</li> <li><code>style.padding</code> (default: <code>12</code>): Inner padding around the text content. For full-bleed cards ( <code>marginHorizontal = 0</code>) with text below the image and all cards with text on the image, padding is applied to all sides of the text. For cards with text below the image where <code>marginHorizontal &gt; 0</code>, padding is applied only to the top and bottom of the text.</li> <li><code>style.marginHorizontal</code> (default: <code>0</code>): Horizontal margin around the card. <code>0</code> means full-bleed.</li> <li><code>style.cornerRadius</code> (default: <code>{theme.primitives.cornerRadius}</code>): Corner radius of the card. The application depends on <code>marginHorizontal</code> and text position. Not applied for full-bleed cards (<code>marginHorizontal=0</code>) with text below the image. Applied to the image for cards with text below the image and <code>marginHorizontal &gt; 0</code>. Applied to the whole card for cards with text on the image and <code>marginHorizontal &gt; 0</code>.</li> <li><code>style.headingsSpacing</code> (default: <code>3</code>): Vertical spacing between the heading and subheading.</li> <li><code>style.buttonSpacing</code> (default: <code>12</code>): Vertical spacing before the button when a card button is present. In the common title/subtitle case, this is the spacing between the headings block and the button.</li> <li><code>style.dynamicTypeEnabled</code> (default: <code>true</code>): Whether cards typography participates in Dynamic Type scaling. When <code>false</code>, heading, subheading, and button text use fixed font sizes and fixed line-height behavior.</li> <li><code>style.backgroundColorLight</code> (optional): Light-mode background color for the text container when <code>textOverContent = false</code> and the card is full-bleed (<code>marginHorizontal = 0</code>).</li> <li><code>style.backgroundColorDark</code> (optional): Dark-mode background color for the text container when <code>textOverContent = false</code> and the card is full-bleed (<code>marginHorizontal = 0</code>).</li> <li><code>style.heading.font</code> (default: <code>{theme.customFont}</code>): Font family for the heading.</li> <li><code>style.heading.textSize</code> (default: <code>22</code>): Font size for the heading.</li> <li><code>style.heading.lineHeight</code> (default: <code>nil</code>): Line height for the heading. If not specified, the font's default line height is used.</li> <li><code>style.heading.textCase</code> (default: <code>default</code>): Text case transformation (<code>upper</code>, <code>lower</code>, <code>default</code>).</li> <li><code>style.heading.letterSpacing</code> (default: <code>0</code>): Letter spacing for the heading.</li> <li><code>style.heading.textColor</code> (default: <code>{theme.colors.white.primary}</code>): Text color for the heading when text is displayed on the background asset.</li> <li><code>style.heading.textBelowContentColorLight</code> (optional): Light-mode heading text color override when <code>textOverContent = false</code>.</li> <li><code>style.heading.textBelowContentColorDark</code> (optional): Dark-mode heading text color override when <code>textOverContent = false</code>.</li> <li><code>style.subHeading.font</code> (default: <code>{theme.customFont}</code>): Font family for the subheading.</li> <li><code>style.subHeading.textSize</code> (default: <code>16</code>): Font size for the subheading.</li> <li><code>style.subHeading.lineHeight</code> (default: <code>nil</code>): Line height for the subheading. If not specified, the font's default line height is used.</li> <li><code>style.subHeading.textCase</code> (default: <code>default</code>): Text case transformation (<code>upper</code>, <code>lower</code>, <code>default</code>).</li> <li><code>style.subHeading.letterSpacing</code> (default: <code>0</code>): Letter spacing for the subheading.</li> <li><code>style.subHeading.textColor</code> (default: <code>{theme.colors.white.secondary}</code>): Text color for the subheading when text is displayed on the background asset.</li> <li><code>style.subHeading.textBelowContentColorLight</code> (optional): Light-mode subheading text color override when <code>textOverContent = false</code>.</li> <li><code>style.subHeading.textBelowContentColorDark</code> (optional): Dark-mode subheading text color override when <code>textOverContent = false</code>.</li> </ul>"},{"location":"Cards/#button-theme-properties","title":"Button Theme Properties","text":"<ul> <li><code>style.button.title.font</code> (default: uses heading font): Font family for the button text. If not specified or null, uses the heading font with the button's text size and line height.</li> <li><code>style.button.title.textSize</code> (default: <code>16</code>): Font size for the button text.</li> <li><code>style.button.title.lineHeight</code> (default: <code>21</code>): Line height for the button text.</li> <li><code>style.button.title.textCase</code> (default: <code>default</code>): Text case transformation for the button text (<code>upper</code>, <code>lower</code>, <code>default</code>).</li> <li><code>style.button.title.letterSpacing</code> (default: <code>0</code>): Letter spacing for the button text.</li> <li><code>style.button.title.textColor</code> (default: <code>{theme.colors.white.primary}</code>): Text color for the button when text is displayed on the background asset.</li> <li><code>style.button.title.textBelowContentColorLight</code> (optional): Light-mode button text color override when <code>textOverContent = false</code>.</li> <li><code>style.button.title.textBelowContentColorDark</code> (optional): Dark-mode button text color override when <code>textOverContent = false</code>.</li> <li><code>style.button.backgroundColor</code> (optional): Background color of the button. If not set, the button will have a transparent background with an outline.</li> <li><code>style.button.outlineColor</code> (default: <code>{theme.colors.white.primary}</code> for text on image, resolved <code>style.button.title.textBelowContentColorLight</code> / <code>style.button.title.textBelowContentColorDark</code> for text below image when provided, otherwise <code>{theme.colors.black.primary}</code> in light mode and <code>{theme.colors.white.primary}</code> in dark mode): Color of the button outline/border. If not specified or null, it follows the theme defaults for text-over-content cards and matches the resolved button title color for text-below-content cards.</li> <li><code>style.button.outlineWidth</code> (default: <code>1</code>): Width of the button outline/border in points.</li> <li><code>style.button.cornerRadius</code> (optional, default: <code>{theme.primitives.cornerRadius}</code>): Corner radius of the button. If null or not set, falls back to the theme's default corner radius.</li> <li><code>style.button.textAlignment</code> (default: uses card <code>textAlignment</code>): Text alignment for the button text. If not specified or null, uses the card's text alignment setting.</li> </ul>"},{"location":"Cards/#behavior-properties","title":"Behavior Properties","text":"<ul> <li><code>behavior.reloading.reloadOnExit</code> (default: <code>true</code>): Whether the Card Collection reloads after returning from tapping a Card (e.g., after dismissing the Story/Clip Player).</li> <li><code>behavior.reloading.reloadOnForeground</code> (default: <code>true</code>): Whether the Card Collection reloads when the app comes to the foreground.</li> </ul>"},{"location":"Changelog/","title":"Changelog","text":""},{"location":"Changelog/#1160-07082026","title":"11.6.0 - 07/08/2026","text":""},{"location":"Changelog/#new-features","title":"New Features","text":"<ul> <li>Added remote theme support for static Clips Player like icons through <code>light/dark.player.icons.like.initial</code> and <code>light/dark.player.icons.like.liked</code> URL pairs. Missing or incomplete remote icon pairs keep the existing default or local theme icons.</li> <li>Added remote caption styling for Stories and Clips through <code>theme.behavior.player.captions</code>, including typography, colors, opacity, padding, and corner radius.</li> <li>Added async <code>StorytellerGAMModuleConfiguration.configureAdRequest</code> so Google Ad Manager integrations can await bidder work and mutate the prepared Google Mobile Ads request before Storyteller loads native, custom-template, or bottom banner GAM Ads</li> <li>Added the new Followable Category profile screen for Clips, including a branded category header, follow/unfollow controls, remotely selectable Stories and Clips content, Latest/Popular paged category feeds, light/dark customization under <code>theme.light.profileScreen</code> and <code>theme.dark.profileScreen</code>, and rollout through <code>theme.behavior.player.clips.enableProfileScreen</code>. See Followable Category Profile theming for more information.</li> </ul>"},{"location":"Changelog/#improvements","title":"Improvements","text":"<ul> <li>Populated the exact <code>adUnitId</code> on GAM and AdMob request, load, failure, paid, and rendered Ad analytics events, including native/banner fallback attempts and GAM VAST tag overrides. Host-provided GAM and AdMob modules can opt into exact request attribution through <code>StorytellerAdRequestTrackingModule</code>. See Ad Unit ID and Request-aware ad modules for more information.</li> <li>Improved Clip Players opened from SDK owned FollowableCategory screen Clips grids so they preserve the originating presentation configuration</li> </ul>"},{"location":"Changelog/#bug-fixes","title":"Bug fixes","text":"<ul> <li>Fixed Stories rows with high poll volume failing to load when poll hydration created an oversized request. Poll hydration now uses bounded requests and preserves successful poll batches when another batch fails.</li> <li>Fixed the tvOS Clip Player losing video, playing mismatched audio, or becoming unresponsive during rapid or backward navigation.</li> <li>Fixed tvOS row focus after dismissing the Story Player or Clip Player, preventing Clips rows from jumping to the beginning and Stories focus from following a viewed story to the end of the row.</li> <li>Fixed the AdChoices icon not responding to taps on AdMob native image and video Ads in Clips.</li> <li>Fixed AdMob native video Ads emitting <code>OpenedAd</code> analytics event on completion without a confirmed replay.</li> <li>Fixed the Story Player title and eyebrow block being misaligned with the header controls.</li> <li>Fixed a crash caused by SDK resource resolution and made missing audio resources non-fatal, allowing image Stories to continue progressing.</li> <li>Fixed Embedded Clips <code>canGoBack</code> so it returns <code>true</code> only when the SDK's internal navigation stack has a previous category screen to pop, and <code>false</code> at the collection root or before navigation is ready. Versions 10.6.0 through 11.5.1 returned the inverse result; integrations that negated it as a workaround should remove that negation when upgrading. See Embedded Clips back navigation for more information.</li> <li>Fixed an issue where scrubbing the Clip Player progress bar could dismiss the Player.</li> </ul>"},{"location":"Changelog/#1154-05082026","title":"11.5.4 - 05/08/2026","text":""},{"location":"Changelog/#bug-fixes_1","title":"Bug fixes","text":"<ul> <li>Fixed a visible hard edge at the bottom of 9:16 video in modal Clips Players configured with <code>theme.behavior.player.clips.modalContentBottomAnchor: screen</code>.</li> </ul>"},{"location":"Changelog/#1153-30072026","title":"11.5.3 - 30/07/2026","text":""},{"location":"Changelog/#improvements_1","title":"Improvements","text":"<ul> <li>Improved Clips reloads through <code>StorytellerClipsViewController.reloadData()</code> and <code>StorytellerClipsModel.reloadData()</code> by preserving the selected For You or Following feed.</li> <li>Improved For You and Following Clips feeds so visible empty feeds refresh after followed categories change.</li> </ul>"},{"location":"Changelog/#bug-fixes_2","title":"Bug fixes","text":"<ul> <li>Fixed Embedded Clips configured with <code>theme.player.clips.progressBar.position: aboveAction</code> so the progress bar no longer prevents taps on a visible primary action.</li> <li>Fixed category navigation from an Embedded Clips Following feed so the selected category loads its complete feed instead of remaining limited to followed clips.</li> </ul>"},{"location":"Changelog/#1152-27072026","title":"11.5.2 - 27/07/2026","text":""},{"location":"Changelog/#bug-fixes_3","title":"Bug fixes","text":"<ul> <li>Fixed <code>Storyteller.shared.user.setFollowedCategories(_:)</code> failing an entire app-managed replacement when Storyteller could not resolve one or more supplied category IDs. Unresolved IDs are now omitted while the resolved subset is applied, and genuine request failures still preserve the previous followed state.</li> </ul>"},{"location":"Changelog/#1151-22072026","title":"11.5.1 - 22/07/2026","text":""},{"location":"Changelog/#new-features_1","title":"New Features","text":"<ul> <li>Added <code>theme.player.clips.topGradient</code> and <code>bottomGradient</code> using the existing <code>Theme.Gradient</code> type, with independent feed-specific, tenant/global, and host light/dark inheritance while preserving the current Clips Player scrims by default. See Clips player gradients for more information.</li> <li>Added <code>theme.player.clips.progressBar.position</code> with <code>bottom</code> and <code>aboveAction</code> values so Embedded Clips can place the progress bar above a visible primary action while preserving the existing layout by default. See Clips progress bar position for more information.</li> <li>Added <code>theme.behavior.player.clips.modalContentBottomAnchor</code> so modal Clips Players can keep the 9:16 media frame unchanged while anchoring lower Player UI to either the video or the screen safe area. See Modal Clips content bottom anchor for more information.</li> <li>Added optional <code>titleToCategoriesSpacing</code>, <code>categoriesToMoreSpacing</code>, <code>metadataToProgressBarSpacing</code>, and <code>progressBarToActionSpacing</code> controls under <code>theme.player.clips.spacing</code> for Embedded Clips. See Embedded Clips spacing for more information.</li> <li>Added optional typography controls for selected and unselected <code>theme.player.clips.feedSwitcher</code> states, <code>theme.player.clips.title.fontWeight</code>, and <code>theme.player.clips.categoryNavigation</code>, including Dynamic Type-scaled line heights and field-by-field remote theme inheritance. <code>StorytellerFontWeight</code> now includes <code>.light</code>; exhaustive client switches must handle the new case when recompiling. See Clips player typography for more information.</li> </ul>"},{"location":"Changelog/#improvements_2","title":"Improvements","text":"<ul> <li>Expanded the supported <code>theme.player.clips.actionIconSize</code> range to <code>24...38</code> while preserving the existing 32pt default and 48pt action-button tap targets.</li> </ul>"},{"location":"Changelog/#1150-01072026","title":"11.5.0 - 01/07/2026","text":""},{"location":"Changelog/#new-features_2","title":"New Features","text":"<ul> <li>Added the <code>StorytellerVASTIntegration</code> module for vendor-neutral fullscreen VAST Ads in Stories and Clips. See Storyteller VAST SDK for setup and behavior details</li> <li>Added <code>StorytellerGAMVASTModule</code> and <code>StorytellerGAMVASTModuleConfiguration</code> so Google Ad Manager VAST tag requests can be configured through the VAST integration without adding Google Mobile Ads or IMA dependencies</li> <li>Added SDK-owned VAST request parameters for GAM VAST tags and Sliide bid enrichment, including size/mute context and host override support. See VAST and GAM VAST Parameter Tables</li> <li>Added the <code>TileVisible</code> analytics event for Story and Clip list tiles, including the 1-based <code>tileIndex</code> payload field. See List Tile Events</li> <li>Added remote theme support for <code>theme.behavior.following.feedSwitcher.selectionStyle</code> so Clips For You / Following switchers can use the legacy selected underline or the new selected text-weight styling. Missing or unknown values default to <code>underline</code>.</li> <li>Added remote theme support for <code>theme.behavior.search.filters</code> so tenant settings can hide Search filter sections while hidden filters reset to their default values.</li> <li>Added tenant-configured Clips opening pre-roll Ads with timeout fallback. See Clips Opening Pre-Roll Ads</li> <li>Added <code>StorytellerClipsAdConfiguration</code> and optional <code>StorytellerClipCollectionConfiguration.adConfiguration</code> so individual Clips presentations can opt into opening pre-roll and bottom banner Ad placements</li> <li>Added <code>Storyteller.shared.user.setFollowedCategories(_:)</code> so app-managed following integrations can asynchronously replace the full followed category set before reloading Storyteller content. See Followed Categories for more information</li> <li>Added remote theme support for <code>theme.behavior.player.clips.categoryNavigation</code> so CMS configuration can customize the delimiter between Clips category navigation labels and optionally hide the leading <code>#</code> in the category Player title</li> </ul>"},{"location":"Changelog/#improvements_3","title":"Improvements","text":"<ul> <li>Improved Card audio toggle placement so it follows the resolved Card theme layout for padding, horizontal margins, and rounded corners</li> <li>Improved SDK startup behavior by deferring I/O operations during initialization</li> </ul>"},{"location":"Changelog/#bug-fixes_4","title":"Bug fixes","text":"<ul> <li>Fixed an issue where unmuted video Cards could restart audio while the host app was in the background</li> <li>Fixed an issue where rapidly reopening a Sheet action could resume playback behind the visible Sheet</li> <li>Fixed a Clips Player issue where Pull to Refresh during fast swiping could leave the Player aligned to a stale clip with broken next/previous navigation</li> <li>Fixed an issue where the Clip Player right-swipe dismiss gesture could stop working after switching from For You to Following and back</li> <li>Fixed an issue where in-app actions opened from a nested search-result Clip Player could briefly reveal the underlying navigation stack while dismissing the Player</li> <li>Fixed an iOS 18 Cards issue where visible video Cards could fail to autoplay because the visibility check treated same-Card SwiftUI hit-test views as overlays</li> <li>Fixed an issue where Card audio toggles could show stale mute state after another Card changed the shared Cards audio state</li> <li>Fixed iPad sheet sizing when rotating with a Sheet open from the Story or Clip Player</li> <li>Fixed an issue where mounted Embedded Clips could keep showing the previous user's feed after the SDK user changed until the host app manually reloaded data</li> <li>Fixed a tvOS Clips Player issue where primary action button titles using custom fonts could truncate after returning from Recents</li> <li>Fixed an issue where Player mute/unmute taps at zero system volume would not toggle the mute button on the Player</li> <li>Fixed a tvOS Clips Player issue where Siri Remote up/down presses could sometimes be ignored</li> </ul>"},{"location":"Changelog/#1141-12062026","title":"11.4.1 - 12/06/2026","text":""},{"location":"Changelog/#new-features_3","title":"New Features","text":"<ul> <li>Added <code>StorytellerGAMModuleConfiguration.publisherProvidedId</code> so Google Ad Manager integrations can pass a request-level Publisher Provided ID (PPID) for audience targeting</li> </ul>"},{"location":"Changelog/#improvements_4","title":"Improvements","text":"<ul> <li>Replaced the <code>lottie-ios</code> dependency with <code>StorytellerLottie</code> for Clips like/unlike animations</li> </ul>"},{"location":"Changelog/#bug-fixes_5","title":"Bug fixes","text":"<ul> <li>Fixed a Clips playback issue where some looped clips could briefly show the first frame again near the end while audio continued playing</li> <li>Fixed an issue where returning to Embedded Clips after updating followed categories could reset the selected tab and leave the Player in an invalid state until Pull to Refresh was triggered</li> </ul>"},{"location":"Changelog/#1140-18052026","title":"11.4.0 - 18/05/2026","text":""},{"location":"Changelog/#new-features_4","title":"New Features","text":"<ul> <li>Added <code>Storyteller.shared.useCustomShareHandling</code> and <code>StorytellerDelegate.onShareButtonTapped(text:title:url)</code> so integrators can intercept Story and Clip share taps, receive the SDK-generated payload, and resume playback with <code>Storyteller.shared.resumePlayer()</code> after their custom share flow is dismissed</li> <li>Added <code>Storyteller.shared.user.getFollowableCategories()</code> to fetch backend-backed followable category metadata and SDK-derived current followed state for custom category management UIs. See Followed Categories for more information</li> <li>Added support for Clips Player instructions onboarding screen</li> <li>Added <code>theme.player.icons.like.animation</code> for Lottie based Clips like/unlike button animations</li> </ul>"},{"location":"Changelog/#improvements_5","title":"Improvements","text":"<ul> <li>Added support for Cards with <code>aspectRatio = \"3:4\"</code></li> <li>Documentation: reorganized the docs navigation and refreshed the landing page</li> <li>Ensured First-Party Ad requests send the same parameters as GAM Ad requests for consistent ad targeting</li> <li>Improved Story and Clip action links so Player destinations open over the current Player instead of dismissing it first</li> <li>Improved network cancellation flows across the SDK</li> </ul>"},{"location":"Changelog/#bug-fixes_6","title":"Bug fixes","text":"<ul> <li>Fixed an issue where reloading Embedded Clips while on a clip ad could hide the collection title on the first clip</li> <li>Fixed an issue where Banner Ads were draggable</li> <li>Fixed an issue with right swipe in certain scenarios in Clips Player</li> <li>Fixed an issue where the Less button would not hide when navigating to another Clip within the Player</li> <li>Fixed an issue where some images might disappear after backgrounding and foregrounding the app</li> <li>Fixed an issue where top UI elements could remain hidden in Clips Player when fast swiping through Non Skippable Ads</li> <li>Fixed an issue where image-based Clips Player titles and visible clip content titles could be skipped by VoiceOver swipe navigation</li> <li>Fixed an issue where Player live badges could be visible for non-live content or content without badge titles</li> <li>Fixed an iPad Clips Player issue where the search/category navigation control could shift outside the Player after rotation and returning from category navigation</li> <li>Fixed an issue where first Card analytics could report an incorrect <code>isMuted</code> value before the first visible video Card settled as primary</li> <li>Added a preventive fix for a rare crash that could occur while images were loading and the <code>AssetDownloader</code> was deinitialized</li> <li>Fixed Clips Player category navigation ordering so the selected/current category is shown last in the list</li> <li>Fixed an issue where AdMob native video controls in the Story Player could trigger Story navigation instead of responding to play/pause or mute taps</li> </ul>"},{"location":"Changelog/#1133-06052026","title":"11.3.3 - 06/05/2026","text":""},{"location":"Changelog/#bug-fixes_7","title":"Bug fixes","text":"<ul> <li>Fixed an issue where video Cards could fail to show the mute toggle if a Cards collection was rendered before SDK initialization completed</li> <li>Fixed an issue where the Cards audio toggle did not animate with the card shrinking animation when pressing or opening a card</li> </ul>"},{"location":"Changelog/#1132-01052026","title":"11.3.2 - 01/05/2026","text":""},{"location":"Changelog/#new-features_5","title":"New Features","text":"<ul> <li>Added Cards audio controls for video Cards, including tenant-driven audio behavior support, known no-audio handling, active-card-only playback, session-scoped mute state, and optional Cards-specific mute-state persistence. See Video Card Audio for more information</li> <li>Added <code>theme.cards.audio.mutedIcon</code> and <code>theme.cards.audio.unmutedIcon</code> to customize Cards audio control icons. See Cards theming for more information</li> <li>Added the <code>CardAudioToggled</code> analytics event with <code>audioToggleFrom</code>, <code>audioToggleTo</code>, and <code>isMuted</code>, and added known <code>isMuted</code> state to video Card analytics events. See Card Audio Toggled for more information</li> </ul>"},{"location":"Changelog/#bug-fixes_8","title":"Bug fixes","text":"<ul> <li>Fixed an issue where the empty Following screen in Clips could hide its CTA</li> </ul>"},{"location":"Changelog/#1131-14042026","title":"11.3.1 - 14/04/2026","text":"<p>Important: <code>11.3.1</code> supersedes <code>11.3.0</code>. Do not use <code>11.3.0</code>, as it shipped with a broken distributed Swift interface in the packaged SDK binary that could prevent consumer apps from importing the SDK.</p>"},{"location":"Changelog/#bug-fixes_9","title":"Bug fixes","text":"<ul> <li>Fixed a packaging issue where the distributed Swift interface emitted by <code>StorytellerSDK</code> could prevent the packaged SDK binary from being imported by consumer apps</li> </ul>"},{"location":"Changelog/#1130-13042026","title":"11.3.0 - 13/04/2026","text":"<p>Superseded by <code>11.3.1</code>. Do not use <code>11.3.0</code>.</p>"},{"location":"Changelog/#breaking-changes","title":"Breaking Changes","text":"<ul> <li>Removed <code>offset</code> from <code>StorytellerStoriesListConfiguration</code> and <code>StorytellerClipsListConfiguration</code></li> </ul>"},{"location":"Changelog/#new-features_6","title":"New Features","text":"<ul> <li>Added <code>theme.engagementUnits.poll.showImageAnswerGradientOverlay</code> to control the gradient overlay behind the text in image Poll Answers (default <code>true</code>). See Themes for more information</li> <li>Added <code>theme.player.enableFollowableCategorySwipeFromRightEdge</code> to control opening Followable Category with right-edge swipe in the Clips Player (default <code>true</code>)</li> <li>Added <code>theme.player.clips.eyebrow</code> to customize the Clips Player eyebrow font family, text size, optional line height, and text color</li> <li>Added <code>theme.player.clips.title</code> to customize the Clips Player main title font family, text size, optional line height, and text color</li> <li>Added <code>theme.player.icons.back</code> to customize the Clips Player back button icon. See Themes for more information</li> <li>Added <code>theme.player.icons.mute.muted</code>, <code>theme.player.icons.mute.unmuted</code>, <code>theme.player.icons.captions.enabled</code>, and <code>theme.player.icons.captions.disabled</code> to customize the Player mute and captions icons in Stories and Clips. See Themes for more information</li> <li>Added <code>theme.player.clips.showButtonBackgrounds</code>, <code>theme.player.clips.actionIconSize</code>, and <code>theme.player.clips.spacing</code> to customize Clips Player button backgrounds, action icon sizing, and layout spacing. See Themes for more information</li> <li>Added Cards theme support for explicit heading/subheading spacing, explicit button spacing, a per-theme Dynamic Type toggle, an optional full-bleed text panel background for text-below-content cards with light/dark background overrides, and appearance-aware heading/subheading/button text colors</li> <li>Added support for AdMob Ads via <code>StorytellerAdMobModule</code>. See Ads for more information</li> <li>Added support for <code>externalApp</code> CTA handling in Custom Native Ads on iOS</li> <li>Added <code>Storyteller.shared.isPlayerMuted</code> to expose the current mute state of the Player</li> <li>Added <code>AdOpportunity</code> analytics event, emitted when the SDK reaches an eligible ad slot before requesting an ad. See Analytics for details</li> <li>Added <code>AdRequested</code> analytics event, emitted when the SDK initiates a Google ads request (GAM/AdMob). See Analytics for details</li> <li>Added <code>AdLoaded</code> analytics event, emitted after a successful ad load with ad source metadata and adapter latency details. See Analytics for details</li> <li>Added <code>AdFailedToLoad</code> analytics event, emitted when a Google ads request fails. See Analytics for details</li> <li>Added <code>AdPaid</code> analytics event, emitted when impression-level paid revenue info is available. See Analytics for details</li> <li>Added <code>AdSessionSummary</code> analytics event, emitted once when a root Storyteller player session ends (Clips or Stories). See Analytics for details</li> <li>Added <code>CardVideoStarted</code>, <code>CardVideoHeartbeat</code>, and <code>CardVideoCompleted</code> analytics events for video cards. See Analytics for details</li> <li>Added <code>StorytellerModule.adSource: StorytellerAdSource?</code> for declaring integrating-app ad sources. See StorytellerModule.adSource for more information</li> <li>Added <code>adSource</code> to ad analytics events. Supported values include <code>storyteller</code> for Storyteller First Party Ads, <code>gam</code>, <code>admob</code>, and custom integrating-app source values. The value is <code>nil</code> when the source is not resolvable (including <code>AdOpportunity</code>, which is emitted before module resolution)</li> </ul>"},{"location":"Changelog/#bug-fixes_10","title":"Bug fixes","text":"<ul> <li>Embedded Clips now fetch and prepend a missing destination clip when it isn't in the first page</li> <li>Fixed an issue where rapid swipes through Clips could prevent clip open tracking from being recorded correctly, which could affect fresh-feed ordering after dismissing the Player</li> <li>Fixed Cards <code>heading.textSize</code> and <code>subHeading.textSize</code> so they still apply when no custom font family is provided</li> <li>Fixed Card video restarting and blinking when the host app triggers a full collection view reload</li> <li>Fixed a rare issue where the video card would not play after a reload</li> <li>Fixed an issue where Clips video ads could emit duplicate <code>OpenedAd</code> and impression tracking for custom native Ads</li> <li>Fixed an issue where the Stories instruction CTA text could be unreadable in dark mode when opening Stories from Search or Followable Categories</li> </ul>"},{"location":"Changelog/#1121-27032026","title":"11.2.1 - 27/03/2026","text":""},{"location":"Changelog/#bug-fixes_11","title":"Bug fixes","text":"<ul> <li>Fixed an issue where LIVE chip could be misaglined in circular tiles</li> </ul>"},{"location":"Changelog/#1120-21012026","title":"11.2.0 - 21/01/2026","text":""},{"location":"Changelog/#new-features_7","title":"New Features","text":"<ul> <li>Added <code>startedAware</code> Story ordering mode that groups Stories by progress: unread \u2192 in progress \u2192 completed. This feature is gated behind the <code>pageReadStatus</code> functional feature flag</li> <li>Added paging functionality to Clips rows and scrollable grids</li> </ul>"},{"location":"Changelog/#improvements_6","title":"Improvements","text":"<ul> <li><code>Skipped Story</code> analytics event is no longer fired when the user is on the last page of a story, as navigating from the last page is not considered a \"skip\". See Analytics for details</li> <li>Improved experience of resuming Stories that were previously opened or fully viewed</li> </ul>"},{"location":"Changelog/#bug-fixes_12","title":"Bug fixes","text":"<ul> <li>Fixed LIVE chip overlapping story title and incorrect corner radius on circular tiles when using large Dynamic Type with automatic row height</li> <li>Fixed Clips bottom banner ads rendering cut off after rotating the device</li> <li>Fixed Story resume selection to use array indices and filtered empty Stories to prevent crashes in Story opens and deeplinks</li> <li>Fixed an issue where the Search results page could be overlapped by Search suggestions when tapping outside of the text field</li> </ul>"},{"location":"Changelog/#1110-26122025","title":"11.1.0 - 26/12/2025","text":""},{"location":"Changelog/#breaking-changes_1","title":"Breaking Changes","text":"<ul> <li>Updated <code>StorytellerTileType</code> to include <code>categories: [StorytellerCategoryDetail]</code> for both <code>.clip</code> and <code>.story</code> cases in <code>StorytellerListViewDelegate.onTileTapped(type:)</code>. For more information, see onTileTapped</li> </ul>"},{"location":"Changelog/#new-features_8","title":"New Features","text":"<ul> <li>Added support for Non Skippable Ads. For more information, see Non Skippable Ads</li> <li>Added <code>setCustomAttributes(_:)</code> method to StorytellerUser for setting multiple custom attributes at once by overwriting current ones</li> <li>Implemented support for the <code>customLiveChipText</code> parameter for Story Tiles, allowing configuration directly via the CMS</li> <li>Added support for showing Banner Ads at the bottom of the Clips Player. See Ads for more information</li> <li>When the Player volume is 0, it will now be treated as muted. The <code>isMuted</code> property in analytics events will be set to <code>true</code></li> <li>Like and Share values on the Clips Player can now be hidden, controlled through the CMS</li> </ul>"},{"location":"Changelog/#bug-fixes_13","title":"Bug fixes","text":"<ul> <li>Fixed an issue where rapid swiping through clips could cause new clips to be inserted before the currently viewed clip</li> <li>Fixed UI issues in the WebView on iOS 26</li> <li>Fixed an issue where calling <code>Storyteller.shared.openCollection</code> with a destination parameter was being ignored</li> <li>Fixed an issue where the Stories Rows and Grids themes were not applied to the Instructions screen</li> <li>Fixed an issue where the Poll Answer View did not occupy the full available width</li> <li>Fixed a case where the Story Player close animation would target the center of the screen instead of the proper Story Tile</li> <li>Fixed an issue where a native video ad might use an incorrect duration from the GAM module</li> <li>Fixed an issue where the Clips Player close animation targeted the tile instead of the center of the screen when an Ad was displayed</li> <li>Fixed an issue where Ad Banners were not accessible when VoiceOver mode was active</li> <li>Constrained round tile live chip custom images to not overflow the tile's bounds and resize to fit</li> <li>Fixed several tvOS focus issues while navigating through the lists and after dismissing the Player</li> <li>Fixed an issue where <code>currentCategory</code> could be missing in analytics for Stories lists configured with the <code>following</code> category</li> <li>Fixed an issue where embedded clips could have no sound or fail to resume playback after dismissing a player presented on top</li> <li>Fixed issues with the Followable Category button where it could remain in a pressed state after a cancelled swipe and the icon was not respecting the theme color</li> </ul>"},{"location":"Changelog/#1105-24112025","title":"11.0.5 - 24/11/2025","text":""},{"location":"Changelog/#bug-fixes_14","title":"Bug fixes","text":"<ul> <li>Fixed an issue where analytics could report a story\u2019s category instead of the configured list category for the <code>currentCategory</code> property</li> </ul>"},{"location":"Changelog/#1104-12112025","title":"11.0.4 - 12/11/2025","text":""},{"location":"Changelog/#improvements_7","title":"Improvements","text":"<ul> <li>Added <code>StorytellerClipCollectionConfiguration.openReason</code> to track the reason a Clip Collection was opened for analytics purposes, such as when opened via external deeplinks</li> </ul>"},{"location":"Changelog/#1103-11112025","title":"11.0.3 - 11/11/2025","text":""},{"location":"Changelog/#bug-fixes_15","title":"Bug fixes","text":"<ul> <li>Resolved an issue where the UIView wrapper for StorytellerCard was incorrectly adding extra safe area padding</li> </ul>"},{"location":"Changelog/#improvements_8","title":"Improvements","text":"<ul> <li>Moved Mute button from the Clips navigation header to the action buttons section</li> <li>Added support for a compact Clips primary action button size, configurable in the CMS</li> </ul>"},{"location":"Changelog/#1102-29102025","title":"11.0.2 - 29/10/2025","text":""},{"location":"Changelog/#bug-fixes_16","title":"Bug fixes","text":"<ul> <li>Fixed an issue where reloading the Following tab with no clips could throw an error</li> <li>Fixed an issue where <code>recalculatedSize</code> could have incorrect values in Grids and Cards views</li> <li>Fixed incorrect progress bar positioning in the Clips player when opened from a list</li> </ul>"},{"location":"Changelog/#improvements_9","title":"Improvements","text":"<ul> <li>Added <code>removeFollowedCategories(_:)</code> to StorytellerUser</li> </ul>"},{"location":"Changelog/#1101-06102025","title":"11.0.1 - 06/10/2025","text":""},{"location":"Changelog/#bug-fixes_17","title":"Bug fixes","text":"<ul> <li>Added support for trackingURL as an optional parameter in GAM adapter, enabling ad rendering functionality regardless of its presence</li> <li>Fixed a memory leak issue affecting Cards when Storyteller was reinitialized multiple times</li> </ul>"},{"location":"Changelog/#improvements_10","title":"Improvements","text":"<ul> <li>Removed <code>StorytellerClipCollectionConfiguration.openReason</code> as it is already passed into the <code>Storyteller.shared.open\u2026</code> methods</li> <li>Improved component layouts for better display with larger accessibility font sizes</li> </ul>"},{"location":"Changelog/#1100-03102025","title":"11.0.0 - 03/10/2025","text":""},{"location":"Changelog/#breaking-changes_2","title":"Breaking Changes","text":"<ul> <li>Removed deprecated code from the public API</li> <li>Removed the default value for <code>isScrollable</code> in SwiftUI grids. You must now explicitly pass <code>isScrollable</code> when initializing <code>StorytellerStoriesGrid</code> and <code>StorytellerClipsGrid</code>.</li> <li>Updated <code>StorytellerListViewDelegate.onTileTapped</code> method to provide richer context via the <code>StorytellerTileType</code> enum. See onTileTapped for more information</li> <li>Changed <code>Storyteller</code> class from using static methods to a shared instance pattern. All API calls must now use <code>Storyteller.shared</code> instead of calling methods directly on the class (e.g., <code>Storyteller.shared.initialize()</code> instead of <code>Storyteller.initialize()</code>)</li> <li><code>eventTrackingOptions</code> can now only be set during SDK initialization by passing a <code>StorytellerEventTrackingOptions</code> object to the <code>initialize</code> method. Runtime modifications are no longer supported. The property remains publicly readable via <code>Storyteller.shared.eventTrackingOptions</code>. To change tracking options, you must reinitialize the SDK. See Privacy and Tracking for more information</li> </ul>"},{"location":"Changelog/#new-features_9","title":"New Features","text":"<ul> <li>The Story player can now display closed captions</li> <li>Introduced a new Following Model where Storyteller is handling all following logic, without the need for storing and syncing followed categories app-side</li> <li>Added support for custom <code>context</code> data in analytics events. Context allows integrators to provide additional analytics attribution data for Storyteller rows, grids and cards. For details, see Context</li> <li>Added automatic user attribute management for followed categories. See Followed Categories for more information</li> <li>Added support for setting default mute state when opening Clip and Story players, and ability to persist mute state between app launches. Both settings are configurable in the CMS</li> <li>Added <code>theme.lists.enablePlayerOpen</code> property to control whether the SDK automatically opens the player when a tile is tapped. See Theme for more information</li> <li>Card collections can now be sorted by viewed/tapped ordering, which means a new Card will be shown every time a Card from the collection has been viewed or tapped.</li> </ul>"},{"location":"Changelog/#improvements_11","title":"Improvements","text":"<ul> <li>Added support for up to 2 lines of text in quiz and poll answers to accommodate longer content</li> <li>Added <code>isCategoryFollowed(_:)</code> method and <code>followedCategories</code> property to StorytellerUser</li> <li>Added support for muting videos in video poll Stories</li> <li>Row views now automatically adjust their height when iOS Dynamic Type text size changes, improving accessibility</li> <li>Added support to show a button for Cards. See Cards for more information</li> <li>Revisiting accessibility in the Clips Player</li> <li>When no limit is provided, non-scrollable grids default to rendering at most 30 items</li> <li>Improved ads analytics tracking with proper predecessor IDs for Stories and Clips</li> <li>Added <code>actionLinkId</code> event property. See Action Link ID for details</li> </ul>"},{"location":"Changelog/#bug-fixes_18","title":"Bug fixes","text":"<ul> <li>Resolved an issue where poll deeplinks with an answer were not handled</li> <li>Fixed an issue where rotating the device on the Search page caused the Stories player layout to break</li> <li>Fixed an issue where sometimes Pull to Refresh was not working after reloading embedded Clips</li> <li>Fixed an issue where Story ad video sound would sometimes continue playing after switching to a different Story page</li> <li>Fixed an issue were Polls with video background were showing the buttons before the video was actually loaded</li> <li>Fixed Poll Image Answer Containers not tappable outside of the buttons, which prevented navigating to the next/previous page</li> <li>Fixed a rare bug where Clip captions could sometimes overlap with the \"For You\"/\"Following\" switcher</li> <li>Fixed an issue where <code>pageActionText</code> appeared in <code>StorytellerUserActivityData</code> even when the Page action did not exist</li> <li>Fixed an issue where applying a search filter could result in a blank search results screen</li> <li>Fixed an issue where the Story player, when opened from lists in horizontal orientation on iOS, could behave incorrectly during playback</li> <li>Fixed a rare issue where new Story Pages wouldn\u2019t appear until host application relaunch</li> <li>Fixed an issue where <code>Muted Ad</code> and <code>Unmuted Ad</code> analytics events were sent when <code>enableAdTracking</code> was disabled. See Privacy and Tracking</li> <li>Fixed an issue where <code>Opened Clip</code> event was not sent when dismissing a Clip Category at the top level</li> <li>Fixed an UI issue where certain Functional Behavior Toggles could render incorrect Clip button states</li> <li>Recovering Cards playback if a Media Services Reset event occurs</li> </ul>"},{"location":"Changelog/#10134-24092025","title":"10.13.4 - 24/09/2025","text":""},{"location":"Changelog/#bug-fixes_19","title":"Bug fixes","text":"<ul> <li>Fixed several Pull to Refresh issues in Clips</li> </ul>"},{"location":"Changelog/#improvements_12","title":"Improvements","text":"<ul> <li>Added <code>StorytellerCardView.recalculatedSize</code> to expose the updated card size after layout calculations</li> <li>A <code>displayLimit</code> of <code>0</code> or negative value is now treated as unlimited (equivalent to passing <code>nil</code>)</li> </ul>"},{"location":"Changelog/#10133-18082025","title":"10.13.3 - 18/08/2025","text":""},{"location":"Changelog/#bug-fixes_20","title":"Bug fixes","text":"<ul> <li>Fixed an issue where the Search suggestions view was missing horizontal padding</li> <li>Fixed an issue where PTR could sometimes trigger on the 2nd clip during fast swiping</li> <li>Fixed an issue where <code>excludeFromAnalytics</code> would not be properly set on analytics events</li> </ul>"},{"location":"Changelog/#10132-12082025","title":"10.13.2 - 12/08/2025","text":""},{"location":"Changelog/#improvements_13","title":"Improvements","text":"<ul> <li>Made guest User ID persistent across login/logout sessions to maintain consistent user experience and viewed history</li> <li>Improved Card text gradient display for better readability when text is overlaid on content</li> </ul>"},{"location":"Changelog/#bug-fixes_21","title":"Bug fixes","text":"<ul> <li>Fixed Search Results screen UI layout issues on iOS 15</li> </ul>"},{"location":"Changelog/#10131-07082025","title":"10.13.1 - 07/08/2025","text":""},{"location":"Changelog/#bug-fixes_22","title":"Bug fixes","text":"<ul> <li>Fixed an issue where Card Video wouldn't properly update when URL changes</li> </ul>"},{"location":"Changelog/#10130-05082025","title":"10.13.0 - 05/08/2025","text":""},{"location":"Changelog/#new-features_10","title":"New Features","text":"<ul> <li>Introduced the Independent Functional Behavior Toggles for better privacy control. For details, see the Privacy and Tracking documentation</li> <li>Updated the Between Pages Ad strategy to insert ads based on viewed page count</li> <li>Introduced an initial index for the <code>betweenStoriesAndPages</code>, <code>betweenPages</code> and <code>betweenClips</code> ads strategies, that will be the index after which the first ad will appear. Can be configured in the CMS.</li> <li>Introduced two new analytics events for enabling and disabling Story captions</li> <li>Added a mute toggle button to the Stories and Clips players for improved audio control, which can be enabled in the CMS</li> <li>Introduced new analytics events for mute actions: <code>Muted Story</code>, <code>Unmuted Story</code>, <code>Muted Clip</code>, <code>Unmuted Clip</code>, <code>Muted Ad</code> and <code>Unmuted Ad</code></li> <li>Added <code>isMuted</code> property to all Story, Clip and Ad analytics events to track the current mute state</li> <li>Introduced a new field in ad-related events, called Ad Strategy</li> <li>Added <code>nextClip</code> parameter to the <code>clips</code> case in <code>StorytellerAdRequestInfo</code> to provide information about the next Clip in the sequence for improved Ad targeting</li> <li>Added support for Followable Category limits, allowing restrictions on the number of categories users can follow</li> <li>Introduced three new analytics events for Followable Category limits: Followable Category Limit Shown, Followable Category Limit Action Button Tapped, and Followable Category Limit Dismissed</li> <li>Added support for Eyebrows - additional contextual text that appears next to the main title of rectangular tiles, Stories and Clips inside the Players.</li> <li>Added the onTileTapped method to <code>StorytellerListViewDelegate</code> to notify the delegate when a tile is tapped before the player is presented</li> <li>Added the <code>adIndex</code> parameter to the <code>clips</code> case in <code>StorytellerAdRequestInfo</code> and to all ad-related analytics events to track the position of ads within a collection</li> <li>Pausing adjacent stories when playing a new one while fast swiping</li> </ul>"},{"location":"Changelog/#improvements_14","title":"Improvements","text":"<ul> <li>Trimmed input values for the GAM module</li> <li>Clip Category images in the Player now download and display sooner</li> </ul>"},{"location":"Changelog/#bug-fixes_23","title":"Bug fixes","text":"<ul> <li>Fixed an issue where the currently displayed Story could occasionally restart and freeze on iPad devices</li> <li>Fixed an issue where the web view navigation buttons did not correctly reflect their enabled or disabled states</li> <li>Fixed an issue where the followable categories might report incorrect following status</li> <li>Fixed a rare issue where the Clips or Stories Player could become unresponsive after rapidly dismissing the share sheet</li> <li>Fixed an issue where an incorrect real time update setting could cause infinite data reload calls</li> <li>Fixed an issue where GAM video ads in Stories would be silent if the device's mute switch was on, even after the user had unmuted Stories via the Volume buttons</li> <li>Fixed an issue where audio from adjacent video Stories would continue playing when fast swiping between them</li> <li>Fixed an issue where video ads continued playing when a controller was presented on top</li> <li>Fixed a rare crash when reloading Stories after an ad was inserted</li> <li>Fixed an issue where <code>SkippedAd</code> and <code>FinishedAd</code> events were not being sent when swiping away from ads in the Clips player</li> <li>Fixed an issue with Search content overlapping on iOS 15 in some cases when using custom fonts</li> <li>Fixed playback issues in scenarios where other content is presented on top of our players, e.g. Search or a Web View</li> <li>Fixed an issue where search analytics events were using incorrect action properties when opened from clips</li> <li>Fixed an issue where Clips could freeze when fast swiping between them</li> </ul>"},{"location":"Changelog/#10122-10072025","title":"10.12.2 - 10/07/2025","text":""},{"location":"Changelog/#bug-fixes_24","title":"Bug fixes","text":"<ul> <li>Fixed a bug where passing a nil User ID when initializing Storyteller would cause the user to reset on each app launch, along with its viewed history</li> <li>Fixed an issue where lists weren't respecting the <code>tiles.title.show</code> configured in the CMS and were always showing tile titles</li> </ul>"},{"location":"Changelog/#10121-01072025","title":"10.12.1 - 01/07/2025","text":"<p>Note - Due to a bug discovered post-release, we do not recommend upgrading to this version. Please use <code>10.12.2</code> or above which contains the fix.</p>"},{"location":"Changelog/#bug-fixes_25","title":"Bug fixes","text":"<ul> <li>Fixed an issue where Storyteller Card <code>onDataLoadComplete</code> callback wouldn't be called when Card data was unchanged</li> </ul>"},{"location":"Changelog/#10120-26062025","title":"10.12.0 - 26/06/2025","text":"<p>Note - Due to a bug discovered post-release, we do not recommend upgrading to this version. Please use <code>10.12.2</code> or above which contains the fix.</p>"},{"location":"Changelog/#improvements_15","title":"Improvements","text":"<ul> <li>Introduced <code>enableRemoteViewingStore</code> option in tracking preferences. For details, see the Privacy and Tracking documentation</li> </ul>"},{"location":"Changelog/#bug-fixes_26","title":"Bug fixes","text":"<ul> <li>Fixed an issue where poll answers were not displayed in Video Stories</li> <li>Fixed an issue where the theme text casing was not applied to the Story CTA button and the CTA on the empty 'Following' screen in Clips</li> </ul>"},{"location":"Changelog/#10111-20062025","title":"10.11.1 - 20/06/2025","text":"<p>Note - Due to a bug discovered post-release, we do not recommend upgrading to this version. Please use <code>10.12.2</code> or above which contains the fix.</p>"},{"location":"Changelog/#bug-fixes_27","title":"Bug fixes","text":"<ul> <li>Fixed an issue where Video Cards wouldn't sometimes properly load or playback would stall</li> </ul>"},{"location":"Changelog/#10110-16062025","title":"10.11.0 - 16/06/2025","text":"<p>Note - Due to a bug discovered post-release, we do not recommend upgrading to this version. Please use <code>10.12.2</code> or above which contains the fix.</p>"},{"location":"Changelog/#bug-fixes_28","title":"Bug fixes","text":"<ul> <li>Fixed an issue where Embedded Clips with \"For You\" / \"Following\" enabled wouldn't resume playback after being reloaded in the background</li> <li>Fixed an issue where the Clip Player wouldn't resume playback after being foregrounded with an alert displayed</li> <li>Fixed some cases when the Clip and Story Player buttons can be pressed multiple times</li> </ul>"},{"location":"Changelog/#improvements_16","title":"Improvements","text":"<ul> <li>Introduced <code>enableFullVideoAnalytics</code> option in tracking preferences. For details, see the Privacy and Tracking documentation</li> <li>Added support for Cards in StorytellerHome</li> <li>Improved privacy and security by hashing all user IDs in the SDK</li> <li>Added Cards support to <code>StorytellerHome</code></li> <li>Implemented SHA256 hashing when storing user ID's, for improved privacy and security</li> </ul>"},{"location":"Changelog/#deprecations","title":"Deprecations","text":"<ul> <li>Deprecated <code>Storyteller.currentUserId</code> property. It now always returns an empty string and will be removed in a future release.</li> <li>Deprecated <code>isAnonymousUser</code> property on <code>StorytellerUserActivityData</code>. It now always returns <code>nil</code> and will be removed in a future release.</li> <li>Deprecated <code>id</code> property on <code>StorytellerAdRequestInfo</code>. It's now always an empty string and will be removed in a future release.</li> </ul>"},{"location":"Changelog/#breaking-changes_3","title":"Breaking Changes","text":"<ul> <li>Removed deprecated public APIs</li> </ul>"},{"location":"Changelog/#10100-26052025","title":"10.10.0 - 26/05/2025","text":""},{"location":"Changelog/#new-features_11","title":"New Features","text":"<ul> <li>Introduced a new <code>StoryInstructionsScreenViewed</code> analytics event</li> <li>Added support for Clip secondary actions</li> <li>Added support for Cards, flexible components for promoting your content</li> <li>Added support to set titles in the CMS for Collections with \"For You\" / \"Following\" enabled</li> <li>Added ability to specify <code>openReason</code> when opening Stories and Clips</li> </ul>"},{"location":"Changelog/#improvements_17","title":"Improvements","text":"<ul> <li>Improved clips loading priority for ForYou/Following mode</li> <li>Added support for first class Sheet actions from the Empty Following screen and from Followable Category actions</li> <li>Updated <code>openedReason</code> analytics property to have value <code>instanceMethod</code> when opening a Story or a Page through <code>Storyteller.openStory(id:)</code> and <code>Storyteller.openPage()</code> methods</li> <li>Added <code>Storyteller.getStoriesCount(for:)</code> method that returns the total number of Stories for provided Category IDs.</li> <li>Added <code>Storyteller.getClipsCount(for:)</code> method that returns the total number of Clips in the Collection.</li> <li>Removed SharePlay option from the Share sheet when sharing stories or clips</li> <li>Ensured the Clips Player Captions button state updates correctly after caption settings are modified in other instances of the Clips Player</li> <li>Added right swipe to dimiss gesture support to the Clips Player</li> </ul>"},{"location":"Changelog/#bug-fixes_29","title":"Bug fixes","text":"<ul> <li>Fixed an issue where clips would not automatically resume playback when returning to the player after executing an <code>inApp</code> clip action</li> <li>Fixed an issue where buttons stayed disabled after swiping past the last or first page inside the Clips player</li> <li>Fixed an issue where the Player wouldn't properly revert audio settings when getting dismissed</li> <li>Fixed an issue where the nearby Clips Player could start playing unexpectedly when \"For You\" or \"Following\" is enabled and the device is rotated</li> <li>Fixed playback and layout issues on the Story and Clips Player that occasionally occurred during device rotation</li> <li>Fixed a bug around clips category navigation and paging where duplicate clips would be appended while navigating through categories</li> </ul>"},{"location":"Changelog/#deprecations_1","title":"Deprecations","text":"<ul> <li>Deprecated <code>Storyteller.isPlayerVisible</code> in favour of Storyteller.isPresentingContent property. <code>Storyteller.isPlayerVisible</code> returns the same value as <code>Storyteller.isPresentingContent</code> and will be removed in a future version.</li> <li>Deprecated <code>Theme.EngagementUnits.Poll.showVoteCount</code> property in favour of a value that can be set in the CMS. Starting from this version, this Theme property has no effect. Default value for the CMS property is <code>true</code>.</li> <li>Deprecated <code>Theme.Tiles.Title.show</code> property in favour of a value that can be set in the CMS. Starting from this version, this Theme property has no effect. Default value for the CMS property is <code>true</code>.</li> <li>Deprecated <code>StorytellerClipsOpenReason</code> enum. Use <code>StorytellerOpenReason</code> instead</li> </ul>"},{"location":"Changelog/#1092-01042025","title":"10.9.2 - 01/04/2025","text":""},{"location":"Changelog/#bug-fixes_30","title":"Bug fixes","text":"<ul> <li>Fixed analytics tracking for Followable Category follow and unfollow actions</li> </ul>"},{"location":"Changelog/#1091-26032025","title":"10.9.1 - 26/03/2025","text":""},{"location":"Changelog/#bug-fixes_31","title":"Bug fixes","text":"<ul> <li>Fixed a crash that occurred when fetching ads in apps built with Swift 6 mode</li> </ul>"},{"location":"Changelog/#1090-24032025","title":"10.9.0 - 24/03/2025","text":""},{"location":"Changelog/#new-features_12","title":"New Features","text":"<ul> <li>Added progress bar to the Clips player</li> <li>Added support for first class Sheets, enabling web content to overlay on top of the current Story or Clip Player</li> </ul>"},{"location":"Changelog/#improvements_18","title":"Improvements","text":"<ul> <li>Introduced <code>enableAdTracking</code> option in tracking preferences. For details, see the Privacy and Tracking documentation</li> <li>Implemented theme support for <code>StorytellerClipsViewController</code>. For details, refer to the Embedded Clips documentation</li> <li>Added continuous play mode for Clips on tvOS, where the player auto-advances to the next clip once the current one finishes</li> <li>Introduced two Player lifecycle methods to the StorytellerDelegate</li> </ul>"},{"location":"Changelog/#bug-fixes_32","title":"Bug fixes","text":"<ul> <li>Fixed VoiceOver reading order of elements in the Clips Player</li> <li>Fixed an invalid app state that could occur when backgrounding the app during interactive push transition from the Clips Player to the Followable Category screen</li> <li>Fixed an issue where Ads would occasionally disappear when switching between \"For You\" and \"Following\" tabs in the Clips Player</li> <li>Resolved audio problems with GAM video Ads when the iPhone mute switch was enabled</li> <li>Fixed truncation of Story timestamps for Stories with longer titles</li> </ul>"},{"location":"Changelog/#1084-180325","title":"10.8.4 - 18/03/25","text":""},{"location":"Changelog/#bug-fixes_33","title":"Bug fixes","text":"<ul> <li>Fixed some crashes occuring in low network conditions</li> </ul>"},{"location":"Changelog/#1083-110325","title":"10.8.3 - 11/03/25","text":""},{"location":"Changelog/#improvements_19","title":"Improvements","text":"<ul> <li>Added support for deeplink URL's to Clips and Stories using external ID's</li> </ul>"},{"location":"Changelog/#1082-250225","title":"10.8.2 - 25/02/25","text":""},{"location":"Changelog/#bug-fixes_34","title":"Bug fixes","text":"<ul> <li>Fixed an issue where duplicate <code>OpenedClip</code> events would be sent in some rare cases when switching from For You to the Following tab in the Clips Player</li> <li>Fixed an issue where Filter Settings were not sent in the <code>DismissedSearch</code> event</li> <li>Fixed an issue where Clip Ads events didn't populate <code>categories</code> property</li> <li>Fixed an issue where <code>SkippedStory</code> didn't trigger when skipping the last page of a Story</li> <li>Fixed an issue where <code>OpenedClip</code> event's <code>openedReason</code> property would be <code>swipe</code> instead of <code>categoryBackTap</code> property when dismissing a Category inside the Clips Player</li> <li>Fixed an issue where <code>NextClip</code> and <code>PreviousClip</code> events would have properties of the Clip user navigated to, instead of the Clip it was navigated from</li> <li>Fixed an issue where transitioning to and from page ads would erroniously record an <code>OpenedStory</code> event</li> </ul>"},{"location":"Changelog/#1081-180225","title":"10.8.1 - 18/02/25","text":""},{"location":"Changelog/#bug-fixes_35","title":"Bug fixes","text":"<ul> <li>Fixed a bug where the wrong <code>AVPlayer</code> would get recorded in some Clips analytics events</li> </ul>"},{"location":"Changelog/#1080-140225","title":"10.8.0 - 14/02/25","text":""},{"location":"Changelog/#new-features_13","title":"New Features","text":"<ul> <li>Added Clips Paging functionality to Clips Player to improve performance and user experience</li> <li>Added support for Swift 6</li> </ul>"},{"location":"Changelog/#improvements_20","title":"Improvements","text":"<ul> <li>Tapping on For You or Following tab when it's already selected will reload that tab's Clips</li> <li>Improved the animation when applying Filters on the Search screen</li> <li>Added <code>clipIndex</code> property to all Ad analytics events and <code>captionsEnabled</code> to all Clips analytics events</li> <li>Added ability to set the <code>Search.heading</code> property in the Theme, refer to the Theme documentation</li> <li>Added default implementation for <code>topLevelBackTapped</code> in the <code>StorytellerClipsViewController</code> to pop it from the nearest navigation controller</li> <li>Added support for Live badge to the Clips Player</li> <li>Replaced the <code>configuration</code> property with the <code>configure</code> method on the Storyteller List Views. <code>configuration</code> property can still be used, but will be deprecated in the next major version</li> <li>Improved Captions button styling on tvOS</li> </ul>"},{"location":"Changelog/#bug-fixes_36","title":"Bug fixes","text":"<ul> <li>Fixed various Clips Player issues that could occur when fast swiping, doing Pull to Refresh or going into the background</li> <li>Fixed the positioning of the action button when the Following tab is empty</li> <li>Fixed an issue where Sharing a quiz link would not open the first Page of the quiz</li> <li>Fixed an issue where Captions sometimes wouldn't be visible if Clip is opened through Category or Search screens</li> <li>Fixed an issue where Search Filters would be applied without tapping the <code>Apply Filters</code> button</li> <li>Fixed an issue where Ads title and Ad label could be misaligned</li> <li>Fixed an issue where status bar on the Search screen wouldn't properly update its styling in light mode</li> <li>Fixed an issue where <code>skippedStory</code> event would be fired when final Page of the Story completes</li> <li>Fixed an issue where Search button in Clips Player could be out of the video frame on iPad in landscape mode</li> <li>Fixed a tvOS issue where Clip title would be too close to the buttons when the Clip title was long</li> <li>Fixed an issue where <code>clipActionType</code> would be <code>none</code> instead of <code>nil</code> for Clips with no action button</li> <li>Fixed an issue where Pull to Refresh wouldn't work in some rare cases inside the Clips Player</li> <li>Fixed an issue where <code>openClip</code> wouldn't be sent when going back from a Category in the Clips Player</li> <li>Fixed an issue where Ads wouldn't disappear when scrolling backwards in the Clips Player</li> <li>Fixed an issue with Search suggestion styling on the Search screen</li> <li>Fixed an issue where the Followable category button wouldn't be clickable in rare scenarios</li> <li>Fixed an issue where the <code>score</code> parameter would be missing when sharing a quiz link</li> </ul>"},{"location":"Changelog/#1077-100225","title":"10.7.7 - 10/02/25","text":""},{"location":"Changelog/#bug-fixes_37","title":"Bug fixes","text":"<ul> <li>Fixed an issue where clips would display an infinite loading spinner when using AirPlay</li> </ul>"},{"location":"Changelog/#1076-280125","title":"10.7.6 - 28/01/25","text":""},{"location":"Changelog/#bug-fixes_38","title":"Bug fixes","text":"<ul> <li>Fixed a bug where the Clips Player would resume playback while presenting a modal screen after returning from background</li> </ul>"},{"location":"Changelog/#1075-230125","title":"10.7.5 - 23/01/25","text":""},{"location":"Changelog/#bug-fixes_39","title":"Bug fixes","text":"<ul> <li>Resolved an issue where clip ads were inserted into the wrong position</li> <li>Corrected round tile labels for accessibility with large text</li> <li>Removed the CC button from live clips</li> </ul>"},{"location":"Changelog/#new-features_14","title":"New Features","text":"<ul> <li>Introduced two new analytics events for enabling and disabling captions</li> </ul>"},{"location":"Changelog/#1074-241224","title":"10.7.4 - 24/12/24","text":""},{"location":"Changelog/#bug-fixes_40","title":"Bug fixes","text":"<ul> <li>Fixed a bug where Pull to Refresh didn't work inside Embedded Clips when the SDK is used from React Native</li> </ul>"},{"location":"Changelog/#1073-171224","title":"10.7.3 - 17/12/24","text":""},{"location":"Changelog/#improvements_21","title":"Improvements","text":"<ul> <li>Added support to show a fixed number of tiles in list rows, via a property called <code>visibleTiles</code>. For details, refer to the StorytellerListViews documentation</li> <li>Adding theme properties to allow integrators to customize appearance of list tiles, badges and the headings of the Instructions screen. For details, refer to the Theme documentation</li> </ul>"},{"location":"Changelog/#bug-fixes_41","title":"Bug fixes","text":"<ul> <li>Fixed a rare crash that could occur while refreshing visible cells</li> <li>Fixed a layout issue in non scrollable grids for round tiles with visible titles</li> </ul>"},{"location":"Changelog/#1072-021224","title":"10.7.2 - 02/12/24","text":""},{"location":"Changelog/#improvements_22","title":"Improvements","text":"<ul> <li>Updating the way custom Followable Category screens are pushed</li> </ul>"},{"location":"Changelog/#1071-271124","title":"10.7.1 - 27/11/24","text":""},{"location":"Changelog/#bug-fixes_42","title":"Bug fixes","text":"<ul> <li>Fixed an issue where the custom Followable Category screen was not being pushed using the integrator's navigation stack</li> </ul>"},{"location":"Changelog/#1070-191124","title":"10.7.0 - 19/11/24","text":""},{"location":"Changelog/#new-features_15","title":"New Features","text":"<ul> <li>Added mechanism to allow integrators to provide a custom Followable Category screen. For details, refer to the Followed Categories documentation</li> <li>Added support for presenting a screen modally on top of the Stories/Clips Player when following an action</li> </ul>"},{"location":"Changelog/#improvements_23","title":"Improvements","text":"<ul> <li>Updated the captions button, allowing users to toggle captions directly from the Clips Player and bypassing the menu</li> <li>Adjusted default font sizes for Stories, Clips, and Singletons for <code>StorytellerHome</code>. For details, refer to the Theme documentation</li> <li>Allowing Clips from lists to support <code>For You</code> and <code>Following</code> tabs</li> <li>Enhanced reorder animations for lists and improved the closing animations for the Clips and Stories Player</li> <li>Removing native Clip Ads after they've been viewed</li> </ul>"},{"location":"Changelog/#bug-fixes_43","title":"Bug fixes","text":"<ul> <li>Fixed an issue where Stories could not be muted</li> <li>Resolved an issue where analytics events were not being triggered consistently when switching between <code>For You</code> and <code>Following</code> tabs</li> <li>Addressed an issue causing the Clips Player to unintentionally display in landscape mode</li> <li>Fixed missing category image when opening a live Clip directly</li> <li>Fixed an issue where the Ad Story could play while the <code>About this Ad</code> screen was displayed</li> <li>Fixed the like button on Clips occasionally showing an incorrect state after navigating to a Clip category</li> <li>Fixed several issues where the Clips Player could enter an invalid state after quickly swiping between <code>For You</code> and <code>Following</code> tabs</li> </ul>"},{"location":"Changelog/#1065-080125","title":"10.6.5 - 08/01/25","text":""},{"location":"Changelog/#improvements_24","title":"Improvements","text":"<ul> <li>Updated <code>containerTitle</code> analytics property to use collection title for clips instead of collection id</li> </ul>"},{"location":"Changelog/#1064-011124","title":"10.6.4 - 01/11/24","text":""},{"location":"Changelog/#improvements_25","title":"Improvements","text":"<ul> <li>Following a category inside <code>For You</code> feed will now update the <code>Following</code> feed</li> </ul>"},{"location":"Changelog/#1063-311024","title":"10.6.3 - 31/10/24","text":""},{"location":"Changelog/#bug-fixes_44","title":"Bug fixes","text":"<ul> <li>Fixed an issue where Lists and Grids wouldn't properly reload in certain cases</li> </ul>"},{"location":"Changelog/#1062-251024","title":"10.6.2 - 25/10/24","text":""},{"location":"Changelog/#new-features_16","title":"New Features","text":"<ul> <li>Added the <code>isLive</code> and <code>collectionClipCount</code> properties to <code>StorytellerUserActivityData</code></li> </ul>"},{"location":"Changelog/#improvements_26","title":"Improvements","text":"<ul> <li>Updated <code>StorytellerGAMModuleConfiguration</code> to accept <code>adUnit</code> as a closure, allowing different Ad Units to be used when fetching Ads</li> </ul>"},{"location":"Changelog/#bug-fixes_45","title":"Bug fixes","text":"<ul> <li>Fixed a crash that occurred during reload animation when swiping quickly through stories</li> <li>Fixed a Player crash when swiping quickly through stories</li> </ul>"},{"location":"Changelog/#1061-151024","title":"10.6.1 - 15/10/24","text":""},{"location":"Changelog/#bug-fixes_46","title":"Bug fixes","text":"<ul> <li>Fixed cells resizing while scrolling for collections with custom content inset behavior</li> </ul>"},{"location":"Changelog/#1060-041024","title":"10.6.0 - 04/10/24","text":""},{"location":"Changelog/#new-features_17","title":"New Features","text":"<ul> <li>Added For You / Following feature to our embedded clips</li> <li>Added auto reloading support to our lists</li> </ul>"},{"location":"Changelog/#improvements_27","title":"Improvements","text":"<ul> <li>Some of our theme parameters can be configured from CMS</li> <li>Updated our theme parameters documentation</li> <li>Forced audio into loudspeaker for stories in certain cases</li> </ul>"},{"location":"Changelog/#bug-fixes_47","title":"Bug fixes","text":"<ul> <li>Fixed standard banner ads not displaying on iPad</li> <li>Fixed a crash due to reorder animation issues</li> </ul>"},{"location":"Changelog/#1053-160924","title":"10.5.3 - 16/09/24","text":""},{"location":"Changelog/#new-features_18","title":"New Features","text":"<ul> <li>Added support for custom URL schemes, allowing direct opening of a Story or Clip</li> </ul>"},{"location":"Changelog/#1052-110924","title":"10.5.2 - 11/09/24","text":""},{"location":"Changelog/#bug-fixes_48","title":"Bug fixes","text":"<ul> <li>Fixed a crash on tvOS when opening the clips player</li> </ul>"},{"location":"Changelog/#1051-090924","title":"10.5.1 - 09/09/24","text":""},{"location":"Changelog/#bug-fixes_49","title":"Bug fixes","text":"<ul> <li>Fixed an issue where followable category images would render incorrectly</li> <li>Fixed an issue where story playback would sometimes be stuck after viewing a Standard Ad</li> <li>Fixed an issue where <code>StorytellerClipsView</code> would erroneously reload after appearing</li> <li>Fixed an issue where Story or Clip action button wouldn't work in some cases when clicked inside Followable Category UI</li> <li>Fixed a crash with non-scrollable grids on iOS 18</li> <li>Fixed a crash when showing a Standard Banner Ad</li> <li>Fixed a crash in the image downloading pipeline</li> <li>Fixed a crash for <code>StorytellerListView</code>'s reorder animation</li> <li>Fixed a crash when switching between stories</li> </ul>"},{"location":"Changelog/#1050-260824","title":"10.5.0 - 26/08/24","text":""},{"location":"Changelog/#new-features_19","title":"New Features","text":"<ul> <li>Supporting GAM Native Ad Templates. See Storyteller Ads for more information</li> <li>Introducing the Storyteller GAM Module. It is used to simplify fetching ads from Google Ads Manager and injecting them inside our Story/Clips Player.</li> <li>This is facilitated by adding a <code>modules</code> array of type StorytellerModule on the <code>Storyteller</code> class which fulfills some of the StorytellerDelegate duties</li> </ul>"},{"location":"Changelog/#improvements_28","title":"Improvements","text":"<ul> <li>Improved loading experience when videos are buffering</li> <li>Updated duration of Player open and close animations</li> <li>Updated <code>configure</code> method on <code>StorytellerClipsViewController</code> to not do any checks on whether to reload the collection or not. It will always reload the collection when called, regardless of the previous value of the <code>StorytellerClipCollectionConfiguration</code> passed in.</li> </ul>"},{"location":"Changelog/#bug-fixes_50","title":"Bug fixes","text":"<ul> <li>Improved text layout in specific scenarios within the Clips Player</li> <li>Fixed an issue where the grid top inset would not be applied correctly on the first load in full-screen mode</li> <li>Fixed an issue where the cell pop animation would be interrupted and cancelled in some scenarios when closing the Player</li> <li>Fixed an issue where playback would be stuck in certain scenarios when coming back into foreground</li> <li>Fixed a crash that would happen in some cases when reusing Storyteller lists when animations were turned on</li> <li>Fixed an analytics bug where <code>FinishedAd</code> would be sent twice when dismissing the Player</li> </ul>"},{"location":"Changelog/#1041-160824","title":"10.4.1 - 16/08/24","text":""},{"location":"Changelog/#bug-fixes_51","title":"Bug fixes","text":"<ul> <li>Fix missing metadata for analytics events</li> </ul>"},{"location":"Changelog/#1040-090824","title":"10.4.0 - 09/08/24","text":""},{"location":"Changelog/#new-features_20","title":"New Features","text":"<ul> <li>Row animations are now enabled by default</li> </ul>"},{"location":"Changelog/#improvements_29","title":"Improvements","text":"<ul> <li>Improved the animation transition when opening the clips player</li> <li>General accessibility improvements across the SDK</li> <li>Incorporated support for Dynamic Type to adjust text size based on user settings</li> <li>Updated analytics fields tied to action button interactions</li> <li>Preloading of clip category icons to optimize the asset loading times</li> </ul>"},{"location":"Changelog/#bug-fixes_52","title":"Bug fixes","text":"<ul> <li>Corrected the poll results label text</li> <li>Fixed a crash occurring when tapping on the more button on the clips player on the iPad</li> </ul>"},{"location":"Changelog/#1031-170724","title":"10.3.1 - 17/07/24","text":""},{"location":"Changelog/#bug-fixes_53","title":"Bug fixes","text":"<ul> <li>Fixing an issue where the live chip badge was not properly centered, but shifted to the left</li> </ul>"},{"location":"Changelog/#1030-150724","title":"10.3.0 - 15/07/24","text":""},{"location":"Changelog/#new-features_21","title":"New Features","text":"<ul> <li>Lists can now reload in an animated fashion, controlled by the animateTilesOnReorder flag in the Theme.</li> <li>Storyteller Home can now have a fixed UI Style set</li> <li>Adding the ability to customize the Clips Player Like button, as well as hiding it entirely. For details, refer to the Theme doc.</li> </ul>"},{"location":"Changelog/#improvements_30","title":"Improvements","text":"<ul> <li>Standardizing Clips and Stories Player present/dismiss transitions</li> <li>Improving the Accessibility experience of the Player</li> <li>Made clarifications around how the User ID is used within the SDK</li> <li>Organized the docs for the Open Player methods</li> <li>Improving the layout and pull-to-refresh behaviour inside Storyteller Home</li> <li>Improving the pull-to-refresh behaviour inside Embedded Clips</li> </ul>"},{"location":"Changelog/#bug-fixes_54","title":"Bug fixes","text":"<ul> <li>Fixing a case where the instructions screen failed to present when opening a deeplink to a Story Category</li> <li>Fixing some Story Player elements not respecting the Theme</li> </ul>"},{"location":"Changelog/#1021-270624","title":"10.2.1 - 27/06/24","text":""},{"location":"Changelog/#improvements_31","title":"Improvements","text":"<ul> <li>when an empty String is passed for <code>StorytellerUserInput</code> in <code>Storyteller.initialize(...)</code> the default user will be used</li> </ul>"},{"location":"Changelog/#1020-19062024","title":"10.2.0 19/06/2024","text":""},{"location":"Changelog/#breaking-changes_4","title":"Breaking Changes","text":"<ul> <li>The following methods on Storyteller have gotten their async version. The old ones are deprecated and will be removed in a future release:</li> <li><code>dismissPlayer(animated:, dismissReason:)</code></li> <li><code>initialize(apiKey:, userInput:)</code></li> <li><code>openDeepLink(url:)</code></li> <li><code>openStory(id:)</code></li> <li><code>openStory(externalId:)</code></li> <li><code>openPage(id:)</code></li> <li><code>openCategory(category:)</code></li> <li><code>openCollection(configuration:)</code></li> <li><code>openClipByExternalId(collectionId:, externalId:)</code></li> <li> <p><code>openSearch()</code></p> </li> <li> <p><code>set(collectionId:initialClipId:openedReason)</code> method on <code>StorytellerClipsViewController</code> has been deprecated in favor of the new method <code>configure(with:)</code> where the configuration parameter is of type <code>StorytellerClipCollectionConfiguration</code>. See EmbeddedClips for more details.</p> </li> <li>removed <code>liveChipImage</code> in favor of the new <code>LiveChip</code> struct where <code>image</code>, <code>textColor</code>, <code>backgroundColor</code> and <code>backgroundGradient</code> can be set. <code>image</code> precedes <code>backgroundGradient</code> and <code>backgroundGradient</code> precedes <code>backgroundColor</code>. See Themes for more details.</li> </ul>"},{"location":"Changelog/#improvements_32","title":"Improvements","text":"<ul> <li>updated the order of some analytics events</li> <li>added the <code>chip.show</code> property to set whether the chip for rectangular tiles is visible. See Themes for more details.</li> <li>added ability to set the open reason when configuring the clip collection, used only for analytics purposes. If not set, default logic will be used.</li> <li>added the <code>canGoBack</code> property to <code>StorytellerClipsViewController</code>. See EmbeddedClips for more details.</li> <li>added ability to set chip text color based on read / unread status</li> <li>improvements to image loading sequence</li> <li>added ability to set live chip properties for rectangular tiles</li> <li>added ability to open collection with a specific category by using <code>Destination</code> property of the <code>StorytellerClipCollectionConfiguration</code></li> </ul>"},{"location":"Changelog/#bug-fixes_55","title":"Bug Fixes","text":"<ul> <li>fixed an issue where the correct font wasn't used for the like count label in Clips Player</li> <li>fixed issues with captions' appearance for longer texts</li> <li>fixed an issue where the Embedded Clip sound would play in the background in rare scenarios</li> <li>fixed a rare crash with analytics</li> <li>fixed an issue with the action button having the wrong color under certain conditions</li> </ul>"},{"location":"Changelog/#1012-130624","title":"10.1.2 13/06/24","text":""},{"location":"Changelog/#bug-fixes_56","title":"Bug Fixes","text":"<ul> <li>fixed a crash due to a race condition when downloading assets</li> </ul>"},{"location":"Changelog/#1011-160524","title":"10.1.1 16/05/24","text":""},{"location":"Changelog/#bug-fixes_57","title":"Bug Fixes","text":"<ul> <li>resolved an issue with the live badge appearance on round cells when using gradients</li> </ul>"},{"location":"Changelog/#1010-150524","title":"10.1.0 15/05/24","text":""},{"location":"Changelog/#new-features_22","title":"New Features","text":"<ul> <li>introduction of First Party Ads</li> <li>added support for Gradient Borders on Circular Tiles</li> </ul>"},{"location":"Changelog/#improvements_33","title":"Improvements","text":"<ul> <li>updated video buffering strategy for enhanced analytics accuracy</li> <li>optimized cell thumbnail fetching to prevent dublicate network calls in rare scenarios</li> </ul>"},{"location":"Changelog/#bug-fixes_58","title":"Bug Fixes","text":"<ul> <li>resolved missing dismiss reason on tvOS clip close analytics event</li> <li>resolved missing <code>currentCategory</code> in analytics events</li> <li>hidden more button for Clip ads</li> </ul>"},{"location":"Changelog/#1001-070524","title":"10.0.1 07/05/24","text":""},{"location":"Changelog/#bug-fixes_59","title":"Bug fixes","text":"<ul> <li>fixing color of Follow button to respect the Theme</li> <li>fixing a crash that appears in some edge cases</li> </ul>"},{"location":"Changelog/#1000-180424","title":"10.0.0 18/04/24","text":""},{"location":"Changelog/#new-features_23","title":"New Features","text":"<ul> <li>introducing Followable Categories</li> <li>Clips now support localized closed captions</li> </ul>"},{"location":"Changelog/#breaking-changes_5","title":"Breaking Changes","text":"<ul> <li>all methods inside the Storyteller class are now static</li> <li>removed deprecated individual properties on our list views, in favour of the new configuration objects</li> <li>the Storyteller and list delegates are now weak. It is up to the integrating apps to reference them strongly</li> </ul>"},{"location":"Changelog/#improvements_34","title":"Improvements","text":"<ul> <li>overall improvements to the player performance and user experience</li> <li>using system images where possible throughout the SDK, thus reducing its size</li> </ul>"},{"location":"Changelog/#bug-fixes_60","title":"Bug fixes","text":"<ul> <li>analytics fixes</li> <li>theming fixes</li> </ul>"},{"location":"Changelog/#9131-020424","title":"9.13.1 02/04/24","text":""},{"location":"Changelog/#improvements_35","title":"Improvements","text":"<ul> <li>fixed the order of some analytics events</li> </ul>"},{"location":"Changelog/#9130-290324","title":"9.13.0 29/03/24","text":""},{"location":"Changelog/#bug-fixes_61","title":"Bug fixes","text":"<ul> <li>fixing clip playback issues while presenting search</li> </ul>"},{"location":"Changelog/#new-features_24","title":"New Features","text":"<ul> <li>adding a new eventTrackingOptions property to Storyteller, allowing more fine-grained control of user privacy options.</li> <li>introducing StorytellerHome, an organized collection of our list views.</li> </ul>"},{"location":"Changelog/#9121-260324","title":"9.12.1 26/03/24","text":""},{"location":"Changelog/#improvements_36","title":"Improvements","text":"<ul> <li>Updated Privacy Manifest file for Required Reasons API requirements</li> <li>Added <code>set(collectionId: String, initialClipId: String?)</code> method in <code>StorytellerClipsViewController</code>. This method allows loading a specific clip within a clip collection directly.</li> <li>Deprecated <code>collectionId</code> property in <code>StorytellerClipsViewController</code> and it will be removed in the future. Use <code>set(collectionId: String, initialClipId: String?)</code> instead.</li> </ul>"},{"location":"Changelog/#9120-200324","title":"9.12.0 20/03/24","text":""},{"location":"Changelog/#bug-fixes_62","title":"Bug fixes","text":"<ul> <li>fixing various gesture related issues for stories on iOS and tvOS</li> <li>layout round cells properly when the title is hidden</li> <li>analytics fixes</li> <li>describing data usage in privacy manifests required by Apple for SDK's</li> </ul>"},{"location":"Changelog/#9110-120324","title":"9.11.0 12/03/24","text":""},{"location":"Changelog/#improvements_37","title":"Improvements","text":"<ul> <li>addressed missing analytics fields and resolved issues with certain events</li> </ul>"},{"location":"Changelog/#9100-280224","title":"9.10.0 28/02/24","text":""},{"location":"Changelog/#improvements_38","title":"Improvements","text":"<ul> <li>analytics improvements: added missing fields, fixed some events</li> </ul>"},{"location":"Changelog/#bug-fixes_63","title":"Bug fixes","text":"<ul> <li>various bug fixes for embedded clips (clips used via <code>StorytellerClipsViewController</code>)</li> <li>fixed rare pausing issues on Polls</li> </ul>"},{"location":"Changelog/#991-130224","title":"9.9.1 13/02/24","text":""},{"location":"Changelog/#bug-fixes_64","title":"Bug fixes","text":"<ul> <li>fixed the data shipped with <code>.shareSucces</code> analytics event</li> </ul>"},{"location":"Changelog/#990-300124","title":"9.9.0 30/01/24","text":""},{"location":"Changelog/#new-features_25","title":"New Features","text":"<ul> <li>added support for Viewed Clips</li> </ul>"},{"location":"Changelog/#breaking-changes_6","title":"Breaking Changes","text":"<ul> <li>changed Theme property <code>storyTiles</code> into <code>tiles</code>. Please see Themes for more details.</li> </ul>"},{"location":"Changelog/#bug-fixes_65","title":"Bug fixes","text":"<ul> <li>UI fixes related to the Search feature</li> <li>fixed issues with pausing the Story player</li> </ul>"},{"location":"Changelog/#981-090124","title":"9.8.1 09/01/24","text":""},{"location":"Changelog/#bug-fixes_66","title":"Bug fixes","text":"<ul> <li>fixed some memory leaks related to Stories</li> <li>bugfix for clipping topInset margin on scrollable grids</li> </ul>"},{"location":"Changelog/#980-131223","title":"9.8.0 - 13/12/23","text":""},{"location":"Changelog/#new-features_26","title":"New Features","text":"<ul> <li>added Pull To Refresh mechanism for <code>StorytellerClipsViewController</code></li> <li>added <code>isScrollable</code> parameter for StorytellerGridViews in SwiftUI</li> <li>added <code>StorytellerClipsViewControllerDelegate</code> to <code>StorytellerClipsViewController</code> to handle data load and finished loading events</li> <li>added reloading to first clip in collection on <code>reloadData</code> call for <code>StorytellerClipsViewController</code></li> </ul>"},{"location":"Changelog/#improvements_39","title":"Improvements","text":"<ul> <li>Improved accessibility for our lists and players</li> </ul>"},{"location":"Changelog/#bug-fixes_67","title":"Bug fixes","text":"<ul> <li>fixed updating read status across multiple rows on player dismissal</li> </ul>"},{"location":"Changelog/#976-221123","title":"9.7.6 - 22/11/23","text":""},{"location":"Changelog/#bug-fixes_68","title":"Bug fixes","text":"<ul> <li>fixed calling <code>reloadData</code> for <code>StorytellerClipsView</code>.</li> </ul>"},{"location":"Changelog/#975-211123","title":"9.7.5 - 21/11/23","text":""},{"location":"Changelog/#bug-fixes_69","title":"Bug fixes","text":"<ul> <li>fixed playing audio after failed <code>StorytellerClipsViewController</code> playback.</li> </ul>"},{"location":"Changelog/#974-091123","title":"9.7.4 - 09/11/23","text":""},{"location":"Changelog/#bug-fixes_70","title":"Bug fixes","text":"<ul> <li>fixed respecting <code>theme.player.showShareButton</code> for Clips.</li> </ul>"},{"location":"Changelog/#973-271023","title":"9.7.3 - 27/10/23","text":""},{"location":"Changelog/#bug-fixes_71","title":"Bug fixes","text":"<ul> <li>fixed some memory leaks related to Clips.</li> </ul>"},{"location":"Changelog/#972-131023","title":"9.7.2 - 13/10/23","text":""},{"location":"Changelog/#bug-fixes_72","title":"Bug fixes","text":"<ul> <li>bug fixes for answer auto-selection in quizzes</li> </ul>"},{"location":"Changelog/#971-101023","title":"9.7.1 - 10/10/23","text":""},{"location":"Changelog/#bug-fixes_73","title":"Bug fixes","text":"<ul> <li>Various bug fixes for embedded clips (clips used via <code>StorytellerClipsViewController</code>)</li> </ul>"},{"location":"Changelog/#970-021023","title":"9.7.0 - 02/10/23","text":""},{"location":"Changelog/#new-features_27","title":"New Features","text":"<ul> <li>added <code>openCategory</code> method, please see Additional Methods for more details</li> <li>added <code>openStory(externalId:)</code> and <code>openClipByExternalId</code> methods, please see Additional Methods for more details</li> <li>added <code>storyDisplayTitle</code> parameter to analytic events, please see Analytics for more details</li> <li>added fixed order rows mode for Stories which can be set up via the CMS</li> <li>added pinned stories support for Stories which can be set up via the CMS</li> </ul>"},{"location":"Changelog/#960-190923","title":"9.6.0 - 19/09/23","text":""},{"location":"Changelog/#new-features_28","title":"New Features","text":"<ul> <li>added preloading of 3 next clips</li> </ul>"},{"location":"Changelog/#improvements_40","title":"Improvements","text":"<ul> <li>updated top and bottom shade gradient on clips</li> <li>more/less button displayed below categories list</li> <li>added sorting stories by ids fallback</li> <li>fixed story progress bar blink</li> </ul>"},{"location":"Changelog/#950-310823","title":"9.5.0 - 31/08/23","text":""},{"location":"Changelog/#new-features_29","title":"New Features","text":"<ul> <li>added support for Page Ads which appear between Pages</li> </ul>"},{"location":"Changelog/#breaking-changes_7","title":"Breaking Changes","text":"<ul> <li>changed signature of ad delegate from <code>getAdsForList</code> to <code>getAd</code>, please see Ads for more details</li> <li>changed classes inside <code>StorytellerAdRequestInfo</code>, <code>StoryInfo</code> and <code>ClipInfo</code> are now single class <code>ItemInfo</code>, please see Ads for more details</li> </ul>"},{"location":"Changelog/#improvements_41","title":"Improvements","text":"<ul> <li>added <code>adPlacement</code> to analytics, please see Analytics for more details</li> </ul>"},{"location":"Changelog/#940-160823","title":"9.4.0 - 16/08/23","text":"<p>New features:</p> <ul> <li>added support for Clips Category Navigation to the Clips Player</li> </ul>"},{"location":"Changelog/#930-200723","title":"9.3.0 - 20/07/23","text":"<p>New features:</p> <ul> <li>Replacing public ad entities prefixes from <code>Client</code> to <code>Storyteller</code>.</li> <li>Added <code>configuration</code> property to setup ListViews. Read more here.</li> <li>Use the default webView's user-agent for tracking pixel requests.</li> <li>Added SwiftUI support. Read more here.</li> </ul> <p>Bug fixes:</p> <ul> <li>Fixed live badge color on clip player.</li> <li>Fixed other apps audio not resuming when mute switch is turned off.</li> <li>Fixed reloading thumbnails on non scrollable grids.</li> </ul>"},{"location":"Changelog/#921-050723","title":"9.2.1 - 05/07/23","text":"<p>New features:</p> <ul> <li>setting custom User-Agent for Tracking Pixels</li> </ul>"},{"location":"Changelog/#920-080623","title":"9.2.0 - 08/06/23","text":"<p>New features:</p> <ul> <li>We have added the <code>StorytellerClipsViewController</code> so you can embed the Clips playback experience in your custom view hierarchy.</li> </ul> <p>Bug fixes:</p> <ul> <li>Fixed some Grids layout issues.</li> <li>Theme fixes for the list tiles.</li> <li>Tracking pixel fixes.</li> </ul> <p>Improvements:</p> <ul> <li>Resuming other apps' music when closing our Stories or Clips player.</li> <li>Updating the Category object for Ads.</li> <li>Introducing the new <code>StorytellerActionType</code> for Ads.</li> <li>Clips playback, analytics and overall improvements.</li> </ul>"},{"location":"Changelog/#917-310523","title":"9.1.7 - 31/05/23","text":"<p>Bug fixes:</p> <ul> <li>Fixed a scroll jump when our non-scrolling grids are incorporated inside a Collection View</li> <li>Fixed a layout issue when round cells are used in grids</li> </ul>"},{"location":"Changelog/#916-250523","title":"9.1.6 - 25/05/23","text":"<p>Bug fixes:</p> <ul> <li>Fixed reusing same ad injected on <code>getAdsForList</code> callback</li> </ul>"},{"location":"Changelog/#915-220523","title":"9.1.5 - 22/05/23","text":"<p>Bug fixes:</p> <ul> <li>Fixed not calling <code>getAdsForList</code> for first clip</li> <li>Fixed applying <code>theme.player.liveChipImage</code></li> </ul> <p>Improvements:</p> <ul> <li>User <code>customAttributes</code> accept only string type values</li> </ul>"},{"location":"Changelog/#914-170523","title":"9.1.4 - 17/05/23","text":"<p>Bug fixes:</p> <ul> <li>Fixed UI issue on the Clip player's action button</li> </ul> <p>Improvements:</p> <ul> <li>Added ad badge which indicates ad on Clips</li> </ul>"},{"location":"Changelog/#913-110523","title":"9.1.3 - 11/05/23","text":"<p>Bug fixes:</p> <ul> <li>Fixed bug where clip ads can get stuck when swiping fast.</li> </ul> <p>Improvements:</p> <ul> <li>Added <code>adType</code> to <code>StorytellerUserActivityData</code> returned on StorytellerDelegate's <code>onUserActivityOccured</code>.</li> <li>Changed interface of <code>StorytellerAdRequestInfo.clips</code>.</li> </ul>"},{"location":"Changelog/#912-080523","title":"9.1.2 - 08/05/23","text":"<p>Bug fixes:</p> <ul> <li>Fixed an occasional crash when opening a story from a deep link.</li> </ul>"},{"location":"Changelog/#911-030523","title":"9.1.1 - 03/05/23","text":"<p>Improvements:</p> <ul> <li>Hiding the Quiz Summary share button when sharing is disabled from the <code>Theme</code>.</li> </ul>"},{"location":"Changelog/#910-020523","title":"9.1.0 - 02/05/23","text":"<p>New features:</p> <ul> <li>We support live streaming Clips now.</li> <li>Exposing the grid's dynamic size via the <code>recalculatedSize</code> property.</li> </ul> <p>Bug Fixes:</p> <ul> <li>Fixed some memory leaks related to Clips.</li> </ul>"},{"location":"Changelog/#905-260423","title":"9.0.5 - 26/04/23","text":"<p>Bug Fixes:</p> <ul> <li>Fixed a bug when stories sometimes weren't showing up in the lists.</li> </ul>"},{"location":"Changelog/#904-190423","title":"9.0.4 - 19/04/23","text":"<p>Bug Fixes:</p> <ul> <li>Fixed a rare bug that could crash the App during a network call</li> </ul>"},{"location":"Changelog/#903-180423","title":"9.0.3 - 18/04/23","text":"<p>Bug Fixes:</p> <ul> <li>Fix rendering of grids when wrapped in SwiftUI views.</li> </ul> <p>Improvements:</p> <ul> <li>Stories and Clips playback improvements.</li> <li>Adding support for creating the List Views in xibs/storyboards.</li> <li>Preserve the categories' order of Story rows &amp; grids.</li> </ul>"},{"location":"Changelog/#902-120423","title":"9.0.2 - 12/04/23","text":"<p>Bug Fixes:</p> <ul> <li>Fixed a crash when the SDK was built in Release mode.</li> <li>Corrected the way top padding works for grids.</li> </ul> <p>Improvements:</p> <ul> <li>Clips memory management improvements.</li> </ul>"},{"location":"Changelog/#901-070423","title":"9.0.1 - 07/04/23","text":"<p>Bug Fixes:</p> <ul> <li>Fixed an issue with the action button on a Story's page.</li> <li>Fixed a problem with the Clip ads.</li> <li>Fixed issue where tapping back in Quiz selected an answer.</li> <li>Fixed issue with score positioning in Quiz.</li> <li>Fixed an issue where some analytic events were not firing for stories.</li> </ul>"},{"location":"Changelog/#900-060423","title":"9.0.0 - 06/04/23","text":"<p>New features:</p> <ul> <li>We introduced a better class hierarchy for the <code>StorytellerListView</code> Classes.</li> </ul> <pre><code> StorytellerListView\n \u251c\u2500\u2500 StorytellerRowView\n \u2502   \u251c\u2500\u2500 StorytellerStoriesRowView\n \u2502   \u2514\u2500\u2500 StorytellerClipsRowView\n \u2514\u2500\u2500 StorytellerGridView\n     \u251c\u2500\u2500 StorytellerStoriesGridView\n     \u2514\u2500\u2500 StorytellerClipsGridView\n</code></pre> <ul> <li>The Grid views now have scrollable and non-scrollable variants.</li> <li>Clips can now contain Ads.</li> <li>Added top and bottom insets for Grids, can be set on <code>StorytellerTheme</code>.</li> <li>Introducing custom user attributes via <code>Storyteller.user</code>.</li> </ul> <p>Bug Fixes:</p> <ul> <li>Fixed an issue when Grid cells weren't updating the read status of stories properly after being watched.</li> </ul> <p>Improvements:</p> <ul> <li>Improved the inner workings of Grids regarding layout.</li> <li>Removed deprecated methods.</li> <li>Stories Lists no longer support <code>collectionId</code> and Clips Lists no longer support <code>categories</code>.</li> <li>Reacting properly to setting <code>overrideUserInterfaceStyle</code> on UIView.</li> <li>Improving gesture handling of the Stories player.</li> </ul>"},{"location":"Changelog/#844-80923","title":"8.4.4 - 8/09/23","text":"<p>Improvements:</p> <ul> <li>adjust to properly work on iOS17</li> </ul>"},{"location":"Changelog/#843-270323","title":"8.4.3 - 27/03/23","text":"<p>Improvements:</p> <ul> <li>Introduced different thumbnail sizes for better story and clip previews</li> </ul>"},{"location":"Changelog/#842-160323","title":"8.4.2 - 16/03/23","text":"<p>Bug Fixes:</p> <ul> <li>Fixed Ads sometimes getting stuck when swiping between stories</li> </ul> <p>Improvements:</p> <ul> <li>Optimized Clips performance for large collections</li> </ul>"},{"location":"Changelog/#841-150223","title":"8.4.1 - 15/02/23","text":"<p>Bug Fixes:</p> <ul> <li>All clip events coming out from <code>onUserActivityOccurred</code> delegate callback contain proper data in <code>StorytellerUserActivityData.categoryDetails</code></li> <li>Fixed <code>theme.lists.backgroundColor</code> not setup correctly for Dark Mode</li> </ul>"},{"location":"Changelog/#840-130123","title":"8.4.0 - 13/01/23","text":"<p>Bug Fixes:</p> <ul> <li>Fixed <code>storyPageCount</code>, <code>storyIndex</code> and <code>storyPlaybackMode</code> not being returned on certain Story events</li> <li>Added missing Clip event data for Opened Clip, Finished Clip and Completed Loop events</li> <li>Fixed story title alignment in some device rotation scenarios</li> </ul>"},{"location":"Changelog/#831-201222","title":"8.3.1 - 20/12/22","text":"<p>Bug Fixes:</p> <ul> <li>added Clip Collection and Clip Category event data for all Clip analytics events</li> </ul> <p>Improvements:</p> <ul> <li>added Current Category to the event data for Story and Ad analytics events</li> </ul>"},{"location":"Changelog/#830-091222","title":"8.3.0 - 09/12/22","text":"<p>Bug Fixes:</p> <ul> <li>fixed default value for <code>answeredMessageTextColor</code></li> </ul> <p>Improvements:</p> <ul> <li>added additional properties to ad events to help identify individual ads</li> <li>modified the <code>getAdsForList</code> callback to provide more ad information</li> <li>exposed information about category types on analytic events</li> <li>improvements to the way a Page's read status is implemented</li> <li>adjusted the timestamp property so timescales are updated for each new Page</li> <li>improvements to the loading time of videos in Stories</li> </ul>"},{"location":"Changelog/#824-301122","title":"8.2.4 - 30/11/22","text":"<p>Improvements:</p> <ul> <li>removed unused <code>onComplete</code> parameter from <code>Storyteller.dismissPlayer</code></li> <li>implemented <code>lists.backgroundColor</code> theme property to change the background color of chips</li> </ul>"},{"location":"Changelog/#823-251122","title":"8.2.3 - 25/11/22","text":"<p>Improvements:</p> <ul> <li>Enforced setting empty UserID to clear local storage</li> </ul>"},{"location":"Changelog/#822-101122","title":"8.2.2 - 10/11/22","text":"<p>Improvements:</p> <ul> <li>Made Clips scrolling experience smoother</li> </ul> <p>Bug Fixes:</p> <ul> <li>Fixed bug with reloadData requests being sent before initialization has finished</li> <li>Fixed bug with StorytellerHome sometimes having incorrect grid sizes</li> <li>Fixed bug with share button on iPads</li> <li>Fixed Action buttons not working in certain cases</li> </ul>"},{"location":"Changelog/#821-011122","title":"8.2.1 - 01/11/22","text":"<p>Bug Fixes:</p> <ul> <li>Fixed bug that prevents Action buttons being used after opening the app from a shared link</li> <li>Fixed <code>getAdViewHandler</code> returning nil on StoryViewModel</li> <li>Fixed rare crash when opening player</li> </ul> <p>Improvements:</p> <ul> <li>Analytics parameters are now sent in the same order and empty values have been removed</li> <li>Shortened Story Poll's remaining time to 2 seconds after an answer has been selected</li> </ul>"},{"location":"Changelog/#820-111022","title":"8.2.0 - 11/10/22","text":"<p>New Features:</p> <ul> <li>Added support for Clips and Collections</li> </ul> <p>Bug Fixes:</p> <ul> <li>Fixed issue that caused <code>DismissedAd</code> activity to not run in certain cases</li> <li>Fixed bug that caused <code>dismissedStory</code> to miss <code>dismissedReason</code> parameters in certain cases</li> <li>Fixed bug that could cause Storyteller to freeze when saving asset to device</li> <li>Fixed bug where tapping to go back on the first Page of the first Story did not start the Page from the start</li> <li>Fixed bug that could cause Story player window to turn white after share activity</li> <li>Fixed setting Categories for a Story row in the demo app</li> <li>Fixed loading icon remaining on screen in certain cases</li> <li>Fixed Story player window becoming white after hiding share activity</li> </ul> <p>Improvements:</p> <ul> <li>Made the selected answer on Polls semitransparent</li> <li>Implemented better logging solution in Xcode console</li> <li>Reduced content length parameter usage</li> <li>Implemented <code>AdActionButtonTapped</code> event</li> <li>Refresh button improvements</li> </ul> <p>Deprecations and Changes:</p> <ul> <li>Renaming <code>StorytellerUserActivity.NewEventType</code> case from .<code>swipedUpOnPage</code> to <code>.pageActionButtonTapped</code></li> <li>Renaming <code>StorytellerUserActivity.EventType.SwipeUp</code> to <code>.ActionButtonTapped</code></li> </ul>"},{"location":"Changelog/#8114-171022","title":"8.1.14 - 17/10/22","text":"<p>New Features:</p> <ul> <li>Added the ability to open a Clip or Collection directly using the <code>openCollection</code> method</li> </ul>"},{"location":"Changelog/#8113-210922","title":"8.1.13 - 21/09/22","text":"<p>Bug Fixes:</p> <ul> <li>Fixed TriviaQuizScore being set as a String instead of an Int</li> <li>Fixed missing text in sharing screen</li> </ul> <p>Deprecations and Changes:</p> <ul> <li>Removed <code>openFeed</code> from <code>Storyteller</code></li> </ul>"},{"location":"Changelog/#8112-010922","title":"8.1.12- 01/09/22","text":"<p>Bug Fixes:</p> <ul> <li>Keeps scroll position of a row when you refresh</li> <li>Fixed Live Story UI and placement bugs</li> </ul>"},{"location":"Changelog/#8111-250822","title":"8.1.11 - 25/08/22","text":"<p>Bug Fixes:</p> <ul> <li>Removed crash possibility when running public sample code</li> <li>Removed the white line on Live Stories row</li> <li>Fixed progress bar disappearing on Stories and ending playback on rare occasions</li> <li>Fixed bug that could make images in emails not visible when sharing Stories</li> <li>Fixed rare bug that could cause Story playback to stall</li> </ul>"},{"location":"Changelog/#8110-240822","title":"8.1.10 - 24/08/22","text":"<p>Bug Fixes:</p> <ul> <li>Fixed bug that could cause a blinking black screen when swiping quickly through Stories</li> <li>Fixed bug with the functionality of the Action buttons</li> </ul>"},{"location":"Changelog/#819-230822","title":"8.1.9 - 23/08/22","text":"<p>Bug Fixes:</p> <ul> <li>Fixed Story sharing bugs</li> </ul> <p>New Features:</p> <ul> <li>Added support for iPad multitasking mode</li> </ul> <p>Improvements:</p> <ul> <li>UI improvements for light and dark mode</li> <li>Made the order of query parameters on requests deterministic</li> </ul>"},{"location":"Changelog/#818-120822","title":"8.1.8 - 12/08/22","text":"<p>Bug Fixes:</p> <ul> <li>fixed issue where the app could get stuck between Stories, showing half of each side of two Stories and a black gap between them</li> <li>fixed issue of watched Stories not moving to the end of the list</li> <li>fixed issue of grids jumping to the bottom of feeds</li> <li>fixed issue with New Chips flickering when going in and out of More Stories grid</li> </ul> <p>UI Changes:</p> <ul> <li>fixed issue with light mode placeholders being used in dark mode for round Story Tiles</li> <li>Story Row container refinements for circular rows, rectangular rows, and Story grids</li> <li>fixed tiles in grids being the wrong aspect ratio</li> <li>fixed issue of Top Stories row appearing as placeholders</li> </ul>"},{"location":"Changelog/#817-100822","title":"8.1.7 - 10/08/22","text":"<p>Bug Fixes:</p> <ul> <li>fixed issue with grids only showing the loading state for 2 tiles</li> </ul> <p>New Features:</p> <ul> <li>added the <code>contentLength</code> parameter to analytic events - see Analytics for more information</li> </ul>"},{"location":"Changelog/#816-100822","title":"8.1.6 - 10/08/22","text":"<p>Bug Fixes:</p> <ul> <li>fixed issue where swiping back to an Ad could make the Ad turn black</li> <li>fixed issue with Ad playing from the point the user stopped watching it at last time the user swipes to it</li> </ul> <p>New Features:</p> <ul> <li>enable/disableTracking code</li> </ul> <p>Deprecations and Changes:</p> <ul> <li>now request ads from the integrating app every few Stories instead of all at once</li> <li>replace <code>dismissClipView</code> and <code>dismissStoryView</code> with <code>dismissPlayer</code> in Storyteller object</li> </ul>"},{"location":"Changelog/#815-050822","title":"8.1.5 - 05/08/22","text":"<p>Bug Fixes:</p> <ul> <li>fixed issue with placeholders not appearing for Story Rows</li> </ul>"},{"location":"Changelog/#814-030822","title":"8.1.4 - 03/08/22","text":"<p>Deprecations and Changes:</p> <ul> <li>remove preloading and <code>specificIdentifier</code> when reloading data</li> </ul>"},{"location":"Changelog/#813-020822","title":"8.1.3 - 02/08/22","text":"<p>Bug Fixes:</p> <ul> <li>fixed issue with playback flailing due to lack of available encoders</li> </ul>"},{"location":"Changelog/#812-020822","title":"8.1.2 - 02/08/22","text":"<p>UI Changes:</p> <ul> <li>added theme properties for circular tile border width</li> <li> <p>adjust <code>theme.storyTiles.liveChip.readImage</code> and <code>theme.storyTiles.liveChip.unreadImage</code> to:</p> </li> <li> <p><code>theme.storyTiles.circularTile.liveChip.readImage</code></p> </li> <li><code>theme.storyTiles.circularTile.liveChip.unreadImage</code></li> <li><code>theme.storyTiles.rectangularTile.liveChip.readImage</code></li> <li><code>theme.storyTiles.rectangularTile.liveChip.unreadImage</code></li> </ul>"},{"location":"Changelog/#811-290722","title":"8.1.1 - 29/07/22","text":"<p>Bug Fixes:</p> <ul> <li> <p>fixes issues related to firing Event Tracking events correctly to provide proper analytics</p> </li> <li> <p><code>OpenedStory</code> was triggered by <code>automaticPlayback</code> as the <code>openedReason</code> and once with <code>storyListTap</code> - it is now only triggered by <code>storyListTap</code></p> </li> <li><code>SkippedPage</code> and <code>CompletedPage</code> were triggered when skipping a page - it is now only triggered by <code>SkippedPage</code></li> <li><code>CompletedStory</code> was triggered when a user finished watching an Ad - no Story events are triggered in this instance</li> <li><code>PausedAdPage</code> was triggered when skipping an Ad - this is now only triggered when the user pauses the Ad</li> <li><code>ViewedAdPageComplete</code> was triggered when skipping an Ad - this is now only triggered when the viewer has viewed the whole Ad</li> <li>fixed issue to ensure <code>triviaQuizQuestionAnswered</code> fires when the users answers a Trivia Quiz or if they time out on a question</li> <li>fixed issue with <code>triviaQuizScore</code> not returning upon <code>triviaQuizCompleted</code></li> <li> <p>fixed issue with <code>skippedAd</code> not firing</p> </li> <li> <p>when closing a Story, issue fixed where the user could return to the wrong position in the list view in the row/grid</p> </li> <li>when sharing a Story Page, fixed an issue where the timer didn't stop to allow the user time to share and return to viewing</li> <li>fixed issue with image answers not appearing on Polls</li> <li>fixed issue with rows with multiple Categories assigned showing Stories swiping into each other and disappearing from the list view</li> <li>fixed issue where Story video preview freezes when swiping back and forth between Stories</li> <li>fixed issue where repeatedly refreshing briefly duplicates rows</li> <li>fixed issue where Story Tiles were incorrectly scaled when displayed across two rows</li> </ul> <p>UI Changes:</p> <ul> <li>displaying instructions screen correctly on iPad</li> <li>fixed an issue where switching between light and dark mode didn't update the theme on circular Story Tiles</li> <li>max length of Story Grid set to 4 Stories in the loading state</li> <li>fixed issue where webview header wasn't changing colour when switching from light to dark mode</li> </ul>"},{"location":"Changelog/#810-130722","title":"8.1.0 - 13/07/22","text":"<p>Bug Fixes:</p> <ul> <li>add empty placeholders when grid view is initialised</li> </ul>"},{"location":"Changelog/#802-110722","title":"8.0.2 - 11/07/22","text":"<p>UI Changes:</p> <ul> <li>when displaying the score of a Trivia Quiz to users, the font size has been made bigger, bolder, and the colour has been changed to white</li> <li>corrected spacing between titles and rows/grids across all devices (set to 12px below and above each title)</li> <li>adjusted left margin to be consistent for titles/rows/grids across all devices</li> </ul> <p>Deprecations and Changes:</p> <ul> <li>removed <code>topInset</code> and <code>bottomInset</code> for grids</li> <li>ignore <code>startInset</code> and end`Inset for rows inside Home</li> </ul>"},{"location":"Changelog/#801-240622","title":"8.0.1 - 24/06/22","text":"<p>UI Changes:</p> <ul> <li>font sizes, cases, and weights corrected</li> <li>adding action buttons instead of swipe ups for Stories</li> <li>support <code>storyTiles.title.show</code> in Story Grids/Rows</li> </ul> <p>New Features:</p> <ul> <li><code>icons.refresh</code> property added to the Story Player - see Themes for more information</li> <li>adding <code>displayLimit</code> parameter to StorytellerListView - please see Configuring a StorytellerListView</li> </ul> <p>Deprecations and Changes:</p> <ul> <li>removed <code>showPercentBarBackground</code> from theme</li> </ul>"},{"location":"Deeplinking/","title":"Deep linking","text":"<p>To make sharing and direct content links work, first complete the shared domain and CMS setup, then implement the URL-receiving route for either UIKit or SwiftUI. Both frameworks use <code>Storyteller.shared.isStorytellerDeepLink(url:)</code> and <code>Storyteller.shared.openDeepLink(url:)</code> after the app receives a URL.</p>"},{"location":"Deeplinking/#understanding-link-types","title":"Understanding Link Types","text":"<p>iOS apps support two types of deep links, each serving different purposes:</p>"},{"location":"Deeplinking/#universal-links-https-urls","title":"Universal Links (HTTPS URLs)","text":"<ul> <li>When to use: When it's unknown whether the user has your app installed (e.g., sharing on social media, email links)</li> <li>Format: <code>https://[tenant_name].shar.estori.es/...</code></li> <li>Behavior: iOS will open your app if installed, otherwise opens the web browser</li> <li>Setup: Requires Associated Domains configuration</li> </ul>"},{"location":"Deeplinking/#url-scheme-links-custom-urls","title":"URL Scheme Links (Custom URLs)","text":"<ul> <li>When to use: When you're certain the user has your app installed (e.g., push notifications, in-app navigation)</li> <li>Format: <code>[tenant_name]stories://...</code></li> <li>Behavior: Directly opens your app; shows an error if not installed</li> <li>Setup: Requires custom URL scheme registration</li> </ul> <p>Important: Push notifications on iOS do not support Universal Links for directly opening apps. You must use URL Scheme Links in push notification payloads to ensure your app opens correctly.</p>"},{"location":"Deeplinking/#add-associated-domain-to-your-project-settings-in-xcode","title":"Add Associated Domain to Your Project Settings in Xcode","text":"<p>At first you need to add associated domain to your projects.</p> <p>1. Go to your project settings in Xcode -&gt; Signing &amp; Capabilities</p> <p></p> <p>2. Press <code>+Capability</code></p> <p>3. Choose <code>Associated Domains</code></p> <p></p> <p>4. Add the following domains:</p> <ul> <li><code>applinks:[tenant_name].ope.nstori.es</code></li> <li><code>applinks:[tenant_name].shar.estori.es</code></li> </ul> <p></p>"},{"location":"Deeplinking/#add-bundle-identifier-to-storyteller-cms","title":"Add Bundle Identifier to Storyteller CMS","text":"<p>After setting up an associated domain you need to add a bundle identifier to Storyteller CMS.</p> <p>1. Log into Storyteller CMS</p> <p>2. Go to <code>Apps</code></p> <p></p> <p>3. Create a new iOS app or edit existing one</p> <p></p> <p>4. Fill out <code>App ID</code></p> <p>App ID has the form <code>&lt;Application Identifier Prefix&gt;.&lt;Bundle Identifier&gt;</code>    e.g. <code>ABCDE12345.com.example.app</code></p> <p></p> <p>5. Press <code>Save</code></p>"},{"location":"Deeplinking/#register-a-custom-url-scheme-for-your-app","title":"Register a Custom URL Scheme for your app","text":"<p>Our SDK supports deeplinking through custom URL schemes. Custom URL schemes allow your application to be launched in a specific context from a custom URL. This is essential for push notifications, as iOS does not support Universal Links from push notifications.</p> <p>In order to use the custom URL scheme supported by our SDK, you need to register it with the following format: <code>[TENANT_NAME]stories://</code>, E.g. <code>gosportsstories://</code>.</p> <p>You can follow the next steps to do so:</p> <p>1. Go to Info tab in your Xcode project settings</p> <p></p> <p>2. Expand URL Types section and add a new URL Type entry. For the <code>Identifier</code> field, you should use a unique identifier, like your app's bundle identifier for example. In <code>URL Schemes</code>, enter <code>[TENANT_NAME]stories</code>, replacing <code>[TENANT_NAME]</code> with your respective Storyteller tenant name. The <code>Role</code> field is only used for macOS applications, and can be ignored on iOS and other platforms.</p> <p></p> <p>After following these steps, your app should be able to directly launch our SDK in a specific context from a URL with a custom scheme.</p> <p>Push Notifications: URL scheme links are essential for opening your app from push notifications. See the Handling URL Scheme Links from Push Notifications section below for implementation details.</p> <p>This feature can be used for example, to directly open a story or a clip with a deeplink url. To directly open a story, the SDK will handle deeplinks with the following format:</p> <ul> <li><code>[TENANT_ID]stories://open/[STORYID]/[PAGEID]</code></li> </ul> <p>Or to open a clip:</p> <ul> <li><code>[TENANT_ID]stories://open/clip/[CLIPID]?collectionId=[COLLECTIONID]</code></li> </ul>"},{"location":"Deeplinking/#handle-links-in-your-app","title":"Handle Links in Your App","text":"<p>StorytellerSDK provides two framework-independent methods:</p> <pre><code>let url = URL(string: \"https://example.shar.estori.es/open/story-id/page-id\")!\nlet isStorytellerLink = Storyteller.shared.isStorytellerDeepLink(url: url)\n</code></pre> <p>This method takes in a URL and returns <code>true</code> if the URL is Storyteller deep link.</p> <pre><code>let url = URL(string: \"https://example.shar.estori.es/open/story-id/page-id\")!\ntry await Storyteller.shared.openDeepLink(url: url)\n</code></pre> <p>This method opens the Story/Clip that was specified in the URL.</p>"},{"location":"Deeplinking/#examples","title":"Examples","text":"<p>Choose the URL-receiving route for your app. UIKit receives Universal Links and custom schemes through app- or scene-delegate methods, depending on the lifecycle your app uses. SwiftUI receives both link types through <code>.onOpenURL</code>.</p> UIKitSwiftUI <p>Apply <code>.onOpenURL</code> to a stable root view. SwiftUI sends both Universal Links and custom URL schemes to this modifier.</p> <pre><code>import StorytellerSDK\nimport SwiftUI\n\n@available(iOS 14.0, *)\nstruct StorytellerAppRootView: View {\n    var body: some View {\n        Text(\"App content\")\n            .onOpenURL { url in\n                openStorytellerURL(url)\n            }\n    }\n\n    private func openStorytellerURL(_ url: URL) {\n        guard Storyteller.shared.isStorytellerDeepLink(url: url) else { return }\n\n        Task { @MainActor in\n            do {\n                try await Storyteller.shared.openDeepLink(url: url)\n            } catch {\n                print(\"Unable to open Storyteller link: \\(error.localizedDescription)\")\n            }\n        }\n    }\n}\n</code></pre> <p>The Showcase app demonstrates forwarding the received URL from <code>.onOpenURL</code> into a shared <code>AppDelegate</code> handler.</p> <p>After completing the shared setup and one framework route, test both an HTTPS Universal Link and your tenant's custom URL scheme.</p>"},{"location":"Deeplinking/#add-handling-deep-link-to-your-appdelegate-or-uiscenedelegate","title":"Add handling deep link to your AppDelegate or UISceneDelegate","text":"<p>Use <code>UIApplicationDelegate</code> when your app owns lifecycle handling there:</p> <pre><code>import UIKit\nimport StorytellerSDK\n\nfinal class AppDelegate: UIResponder, UIApplicationDelegate {\n    func application(\n        _ application: UIApplication,\n        continue userActivity: NSUserActivity,\n        restorationHandler: @escaping ([UIUserActivityRestoring]?) -&gt; Void\n    ) -&gt; Bool {\n        guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,\n              let url = userActivity.webpageURL else {\n            return false\n        }\n\n        return openStorytellerURL(url)\n    }\n\n    func application(\n        _ app: UIApplication,\n        open url: URL,\n        options: [UIApplication.OpenURLOptionsKey: Any] = [:]\n    ) -&gt; Bool {\n        openStorytellerURL(url)\n    }\n\n    private func openStorytellerURL(_ url: URL) -&gt; Bool {\n        guard Storyteller.shared.isStorytellerDeepLink(url: url) else {\n            return false\n        }\n\n        Task { @MainActor in\n            do {\n                try await Storyteller.shared.openDeepLink(url: url)\n            } catch {\n                print(\"Unable to open Storyteller link: \\(error.localizedDescription)\")\n            }\n        }\n        return true\n    }\n}\n</code></pre> <p>If your app uses scenes, add cold-start handling to your existing <code>scene(_:willConnectTo:options:)</code> implementation and keep the continuation methods for links received while the scene is already connected:</p> <pre><code>import UIKit\nimport StorytellerSDK\n\nfinal class SceneDelegate: UIResponder, UIWindowSceneDelegate {\n    func scene(\n        _ scene: UIScene,\n        willConnectTo session: UISceneSession,\n        options connectionOptions: UIScene.ConnectionOptions\n    ) {\n        if let url = connectionOptions.userActivities.lazy\n            .filter({ $0.activityType == NSUserActivityTypeBrowsingWeb })\n            .compactMap(\\.webpageURL)\n            .first(where: { Storyteller.shared.isStorytellerDeepLink(url: $0) }) {\n            openStorytellerURL(url)\n            return\n        }\n\n        guard let url = connectionOptions.urlContexts.lazy\n            .map(\\.url)\n            .first(where: { Storyteller.shared.isStorytellerDeepLink(url: $0) })\n        else {\n            return\n        }\n\n        openStorytellerURL(url)\n    }\n\n    func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {\n        guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,\n              let url = userActivity.webpageURL else {\n            return\n        }\n\n        openStorytellerURL(url)\n    }\n\n    func scene(_ scene: UIScene, openURLContexts URLContexts: Set&lt;UIOpenURLContext&gt;) {\n        guard let url = URLContexts.lazy\n            .map(\\.url)\n            .first(where: { Storyteller.shared.isStorytellerDeepLink(url: $0) })\n        else {\n            return\n        }\n\n        openStorytellerURL(url)\n    }\n\n    private func openStorytellerURL(_ url: URL) {\n        guard Storyteller.shared.isStorytellerDeepLink(url: url) else { return }\n\n        Task { @MainActor in\n            do {\n                try await Storyteller.shared.openDeepLink(url: url)\n            } catch {\n                print(\"Unable to open Storyteller link: \\(error.localizedDescription)\")\n            }\n        }\n    }\n}\n</code></pre> <p>For a UIKit app entrypoint using CocoaPods, see the Showcase <code>AppDelegate</code>.</p>"},{"location":"Deeplinking/#handling-url-scheme-links-from-push-notifications","title":"Handling URL Scheme Links from Push Notifications","text":"<p>When using push notifications to deep link into Storyteller content, you must use URL scheme links (not Universal Links) in your notification payload. Here's how to implement this:</p>"},{"location":"Deeplinking/#push-notification-payload","title":"Push Notification Payload","text":"<p>Include a custom URL scheme link in your push notification payload:</p> <pre><code>{\n    \"aps\": {\n        \"alert\": {\n            \"title\": \"Check out this story!\",\n            \"body\": \"Tap to view the latest content\"\n        }\n    },\n    \"deeplink_url\": \"[tenant_name]stories://open/STORY_ID/PAGE_ID\"\n}\n</code></pre>"},{"location":"Deeplinking/#handling-the-deep-link","title":"Handling the Deep Link","text":"<p>The notification should open the custom-scheme URL through the same framework route configured in Handle Links in Your App:</p> <ul> <li>UIKit routes the URL to <code>application(_:open:options:)</code>.</li> <li>UIKit apps using scenes route it to <code>scene(_:openURLContexts:)</code> instead.</li> <li>SwiftUI routes the URL to <code>.onOpenURL</code>.</li> </ul> <p>There is no second Storyteller integration path for push notifications. Extract the URL from the notification payload, ask the system to open it, and let your existing URL handler validate and open the Storyteller content.</p>"},{"location":"Deeplinking/#extracting-deep-links-from-push-notifications","title":"Extracting Deep Links from Push Notifications","text":"<p>In your <code>UNUserNotificationCenterDelegate</code>:</p> <pre><code>@MainActor\nfunc userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async {\n    let userInfo = response.notification.request.content.userInfo\n\n    guard\n        let deepLink = userInfo[\"deeplink_url\"] as? String,\n        let url = URL(string: deepLink)\n    else {\n        return\n    }\n\n    // This will trigger onOpenURL in SwiftUI or application(_:open:options:) in UIKit\n    await UIApplication.shared.open(url)\n}\n</code></pre>"},{"location":"Deeplinking/#deep-link-handling-details","title":"Deep Link Handling Details","text":"<p>The <code>Storyteller.shared.openDeepLink</code> function intelligently parses the provided URL (which can be either an HTTPS link via Associated Domains or a custom scheme link) to determine the type of content to open.</p>"},{"location":"Deeplinking/#story-category","title":"Story Category","text":"<ul> <li>Identifies links containing <code>/open/category/</code> or <code>/go/category/</code> in the path.</li> <li>Extracts the category identifier following <code>/category/</code>.</li> <li>Calls the internal equivalent of <code>Storyteller.shared.openCategory</code> with the extracted category ID.</li> <li>Example HTTPS: <code>https://[tenantname].shar.estori.es/go/category/123456</code></li> <li>Example Custom Scheme: <code>[tenantname]stories://open/category/123456</code></li> </ul>"},{"location":"Deeplinking/#clip-collection","title":"Clip Collection","text":"<ul> <li>Identifies links containing <code>/open/clip</code>, <code>/go/clip</code>, <code>/open/clips</code>, or <code>/go/clips</code> in the path.</li> <li>Requires a <code>collectionId</code> query parameter.</li> <li>Optionally accepts a <code>categoryId</code> query parameter to specify an initial category.</li> <li>Optionally accepts a <code>clipId</code> path segment to attempt opening a specific clip within the collection.</li> <li>Calls the internal equivalent of <code>Storyteller.shared.openCollection</code> using the extracted information.</li> <li>Example HTTPS: <code>https://[tenantname].shar.estori.es/open/clip/CLIP_UUID?collectionId=COLLECTION_ID&amp;categoryId=CATEGORY_ID</code></li> <li>Example Custom Scheme: <code>[tenantname]stories://open/clip/CLIP_UUID?collectionId=COLLECTION_ID&amp;categoryId=CATEGORY_ID</code></li> </ul>"},{"location":"Deeplinking/#story-page","title":"Story / Page","text":"<ul> <li>Identifies links matching patterns like <code>/story/STORY_ID</code> or <code>/page/PAGE_ID</code> (for HTTPS) or <code>open/STORY_ID/PAGE_ID</code> (for custom scheme).</li> <li>Extracts the <code>storyId</code> and/or <code>pageId</code> from the path segments.</li> <li>Calls the internal equivalent of <code>Storyteller.shared.openStory(id:)</code> or <code>Storyteller.shared.openPage(id:)</code>.</li> <li>Example HTTPS (Story): <code>https://[tenantname].shar.estori.es/story/STORY_UUID</code></li> <li>Example HTTPS (Page): <code>https://[tenantname].shar.estori.es/page/PAGE_UUID</code></li> <li>Example Custom Scheme: <code>[tenantname]stories://open/STORY_UUID/PAGE_UUID</code></li> </ul>"},{"location":"Deeplinking/#sheet","title":"Sheet","text":"<ul> <li>Identifies links containing <code>/open/sheet/</code> or <code>/go/sheet/</code> in the path.</li> <li>Extracts the <code>sheetId</code> from the path segment following <code>/sheet/</code>.</li> <li>Calls the internal equivalent of <code>Storyteller.shared.openSheet(id:)</code>.</li> <li>Example HTTPS: <code>https://[tenantname].ope.nstori.es/open/sheet/SHEET_ID</code></li> <li>Example Custom Scheme: <code>[tenantname]stories://open/sheet/SHEET_ID</code></li> </ul>"},{"location":"Deeplinking/#manual-deep-link-handling","title":"Manual Deep Link Handling","text":"<p>While <code>Storyteller.shared.openDeepLink</code> provides convenience, you might require more control over your app's state or navigation when a deep link is handled. In such cases, it's recommended to parse the URL yourself (after checking it with <code>Storyteller.shared.isStorytellerDeepLink</code>) and then use the specific Storyteller methods like <code>openStory(id:)</code>, <code>openPage(id:)</code>, <code>openCollection(configuration:)</code>, <code>openCategory(category:)</code>, or <code>openSheet(id:)</code> to present the content. This approach allows for custom transitions, loading states, or error handling specific to your application flow. Refer to the Open Player documentation for details on these methods.</p>"},{"location":"Deeplinking/#api-reference","title":"API Reference","text":""},{"location":"Deeplinking/#isstorytellerdeeplink","title":"isStorytellerDeepLink","text":"<pre><code>let url = URL(string: \"exampletenantstories://open/story-id/page-id\")!\nlet isStorytellerLink = Storyteller.shared.isStorytellerDeepLink(url: url)\n</code></pre> <p>Checks if the given url is Storyteller deep link.</p>"},{"location":"Deeplinking/#opendeeplink","title":"openDeepLink","text":"<pre><code>let url = URL(string: \"exampletenantstories://open/story-id/page-id\")!\ntry await Storyteller.shared.openDeepLink(url: url)\n</code></pre> <p>This call makes Storyteller open the provided deep link (showing the requested Page / Story / Clip).</p> <p>Parameters:</p> <ol> <li><code>url</code> - deep link url.</li> </ol> <p>Throws if there is an issue with opening the Deeplink (e.g. the requested content is not available).</p>"},{"location":"EmbeddedClips/","title":"Embedded Clips","text":"<p>Use Embedded Clips when you want the full Clips Player inside your own view hierarchy rather than presenting it programmatically. UIKit apps use <code>StorytellerClipsViewController</code>; SwiftUI apps use <code>StorytellerClipsView</code> with a <code>StorytellerClipsModel</code>.</p> <p>If you need Clip tiles in a row or grid rather than an embedded Player, use Storyteller List Views.</p> <p><code>topLevelBackButtonEnabled</code> defaults to <code>false</code> in both framework routes.</p>"},{"location":"EmbeddedClips/#adding-it-in-the-ui","title":"Adding it in the UI","text":"UIKitSwiftUI <p>Place <code>StorytellerClipsViewController</code> inside a <code>UITabBarController</code>:</p> <pre><code>let embeddedClipsVC = StorytellerClipsViewController()\nlet tabBarVC = UITabBarController()\nlet otherVC = UIViewController()\ntabBarVC.setViewControllers([embeddedClipsVC, otherVC], animated: false)\n</code></pre> <p>Or add it to a custom view-controller hierarchy:</p> <pre><code>final class ClipsHostViewController: UIViewController {\n    private let embeddedClipsVC = StorytellerClipsViewController()\n\n    override func viewDidLoad() {\n        super.viewDidLoad()\n\n        addChild(embeddedClipsVC)\n        view.addSubview(embeddedClipsVC.view)\n        // Add Auto Layout constraints here.\n        embeddedClipsVC.didMove(toParent: self)\n    }\n}\n</code></pre> <p><code>StorytellerClipsViewController</code> exposes:</p> <ul> <li><code>topLevelBackButtonEnabled</code> - whether the back button should be displayed when the player is at the top level of the collection.</li> <li><code>canGoBack</code> - whether the SDK's internal navigation stack has a previous category screen to pop.</li> <li><code>delegate</code> - the weak <code>StorytellerClipsViewControllerDelegate</code> used for loading and navigation callbacks.</li> </ul> <p>For a UIKit integration using CocoaPods, see <code>EmbeddedClipsViewController</code>.</p> <p>Create and retain a <code>StorytellerClipsModel</code>, then pass it to <code>StorytellerClipsView</code>. Handle actions in the view callback rather than assigning a delegate.</p> <pre><code>import SwiftUI\nimport StorytellerSDK\n\n@available(iOS 14.0, *)\nstruct ExampleSwiftUIView: View {\n    @Environment(\\.presentationMode) private var presentationMode\n    @StateObject var model = StorytellerClipsModel(\n        configuration: StorytellerClipCollectionConfiguration(collectionId: \"test-collection\"),\n        topLevelBackButtonEnabled: true\n    )\n\n    var body: some View {\n        StorytellerClipsView(model: model) { action in\n            if case .onTopLevelBackTapped = action {\n                presentationMode.wrappedValue.dismiss()\n            }\n        }\n    }\n}\n</code></pre> <p><code>StorytellerClipsModel</code> exposes the shared <code>configuration</code>, <code>topLevelBackButtonEnabled</code>, and <code>canGoBack</code> values, plus <code>reloadData()</code>, <code>willShow()</code>, and <code>willHide()</code>.</p> <p>For a SwiftUI integration with configuration, reload cadence, and context injection, see the Showcase implementation in <code>ClipsViewModel</code> and <code>ClipsView</code>.</p>"},{"location":"EmbeddedClips/#back-navigation","title":"Back navigation","text":"<p><code>canGoBack</code> is a read-only capability query; it does not perform navigation and it does not describe the host app's navigation stack. When it is <code>true</code>, the SDK owns navigation back from the current nested category screen. At the collection root it is <code>false</code>, even if the host app itself has somewhere to navigate back to.</p> <p>To expose host-owned back navigation at the collection root, set <code>topLevelBackButtonEnabled</code> to <code>true</code>, then handle the callback for your framework:</p> UIKitSwiftUI <p>Handle <code>topLevelBackTapped()</code> on the delegate. The default implementation for delegates that are also <code>UIViewController</code> instances pops the nearest host navigation controller. Override it for a custom route:</p> <pre><code>import StorytellerSDK\nimport UIKit\n\nfinal class ClipsHostViewController: UIViewController, StorytellerClipsViewControllerDelegate {\n    func topLevelBackTapped() {\n        navigationController?.popViewController(animated: true)\n    }\n\n    func onDataLoadStarted() {}\n\n    func onDataLoadComplete(success: Bool, error: Error?) {}\n}\n</code></pre> <p>Handle <code>.onTopLevelBackTapped</code> in the <code>StorytellerClipsView</code> action closure and perform host-owned navigation, as shown in the SwiftUI setup above. <code>model.canGoBack</code> has the same root and nested behavior as the UIKit property.</p> <p>In SDK versions 10.6.0 through 11.5.1, <code>canGoBack</code> returned the inverse result because of a regression. Version 11.6.0 restores the contract described above. If your integration negated <code>canGoBack</code> as a workaround, remove that negation when upgrading.</p>"},{"location":"EmbeddedClips/#loading-clips","title":"Loading Clips","text":"<p>Use <code>StorytellerClipCollectionConfiguration</code> when configuring an embedded or programmatically presented Clip Player. Use <code>StorytellerClipsListConfiguration</code> only for Clip row and grid tile views such as <code>StorytellerClipsRowView</code> and <code>StorytellerClipsGridView</code>. The types are not interchangeable: list configuration controls tile layout and limits, while collection configuration controls Player destination, presentation theme, analytics context, and Ad placements.</p> <p><code>StorytellerClipCollectionConfiguration</code> has the following properties:</p> <ul> <li><code>collectionId</code> - the id of the collection to be displayed.</li> <li><code>destination</code> - specify which <code>clip</code> or <code>category</code> to show when opening the collection. If <code>clip</code> or <code>category</code> is not specified or wrong value is set, we default to the first clip.</li> <li><code>theme</code> - This property defines the overall appearance of the StorytellerClipsViewController. If this property is not specified, the global theme will be applied by default.</li> <li><code>openReason</code> - Action type used for analytics to track why the collection was opened. If nil, the open reason is handled internally.</li> <li><code>context</code> - Optional context data that will be included in analytics callbacks for attribution.</li> <li><code>adConfiguration</code> - Optional per-presentation Ad placement controls. When omitted or set to <code>nil</code>, opening pre-roll and bottom banner Ads are not opted in locally. Pass <code>StorytellerClipsAdConfiguration</code> to opt into individual placements when the tenant is remotely configured for the matching Clips Ad placement.</li> </ul> <p>Apply the shared configuration through the route for your framework:</p> UIKitSwiftUI <p>Call <code>configure(with:)</code> on the embedded view controller:</p> <pre><code>let embeddedClipsVC = StorytellerClipsViewController()\nlet configuration = StorytellerClipCollectionConfiguration(collectionId: \"top-plays\")\nembeddedClipsVC.configure(with: configuration)\n</code></pre> <p>Calling <code>configure(with:)</code> does not reload data when <code>collectionId</code> and <code>destination</code> are unchanged. Call <code>reloadData()</code> to force a refresh.</p> <p>Supply the configuration when creating <code>StorytellerClipsModel</code>. Update the model or call its methods to control the existing embedded Player:</p> <pre><code>let model = StorytellerClipsModel(\n    configuration: StorytellerClipCollectionConfiguration(collectionId: \"collection-id\"),\n    topLevelBackButtonEnabled: true\n)\n\nlet configuration = model.configuration\nlet showsTopLevelBackButton = model.topLevelBackButtonEnabled\nlet canGoBack = model.canGoBack\n\nmodel.reloadData()\nmodel.willShow()\nmodel.willHide()\n</code></pre> <p>SwiftUI reloads when the supplied model changes. Call <code>model.reloadData()</code> to refresh without changing its configuration.</p> <p>To opt an embedded Clips presentation into opening pre-roll Ads, set the placement on the shared configuration:</p> <pre><code>let preRollConfiguration = StorytellerClipCollectionConfiguration(\n    collectionId: \"top-plays\",\n    adConfiguration: StorytellerClipsAdConfiguration(preRollEnabled: true)\n)\n</code></pre> <p>To opt an embedded Clips presentation into bottom banner Ads:</p> <pre><code>let bottomBannerConfiguration = StorytellerClipCollectionConfiguration(\n    collectionId: \"top-plays\",\n    adConfiguration: StorytellerClipsAdConfiguration(bottomBannerEnabled: true)\n)\n</code></pre> <p>Supply either configuration to the UIKit view controller or SwiftUI model. Local opt-in does not replace the matching tenant and provider configuration described in Ads.</p> <p>Possible values for <code>StorytellerClipCollectionConfiguration.Destination</code> are:</p> <pre><code>let categoryDestination: StorytellerClipCollectionConfiguration.Destination = .category(id: \"category-id\")\nlet clipDestination: StorytellerClipCollectionConfiguration.Destination = .clip(id: \"clip-id\")\n</code></pre> <p>An invalid or unavailable destination falls back to the first Clip.</p>"},{"location":"EmbeddedClips/#auto-pause","title":"Auto-pause","text":"<p>Besides when the user pauses the Clips Player, the SDK automatically pauses or plays based on visibility changes, such as backgrounding the app or switching tabs. If you have a scenario that is not covered, such as displaying a modal over the Player, send visibility hints through the UIKit view controller or SwiftUI model:</p> <ul> <li><code>willHide()</code> - Call this before the Player becomes obscured or inactive.</li> <li><code>willShow()</code> - Call this when the Player becomes visible or active again.</li> </ul> <p>Note: Auto-pausing respects the user's explicit pause state. If the user manually paused, <code>willShow()</code> will not automatically resume playback.</p>"},{"location":"EmbeddedClips/#reloaddata","title":"ReloadData","text":"<p>Call <code>reloadData()</code> on the UIKit view controller or SwiftUI model to trigger a new request and reset the Player to its initial state.</p> <p>Note: If category filters are currently applied when <code>reloadData()</code> is called, the player will navigate back one level in the category stack instead of reloading the entire collection. If no category filters are applied, it reloads the top-level collection data.</p>"},{"location":"EmbeddedClips/#delegate","title":"Delegate","text":"<p>Use the callback mechanism for your framework:</p> Event UIKit SwiftUI Loading starts <code>StorytellerClipsViewControllerDelegate.onDataLoadStarted()</code> <code>.onDataLoadStarted</code> action Loading completes <code>StorytellerClipsViewControllerDelegate.onDataLoadComplete(success:error:)</code> <code>.onDataLoadComplete(success:error:)</code> action Top-level back tapped <code>StorytellerClipsViewControllerDelegate.topLevelBackTapped()</code> <code>.onTopLevelBackTapped</code> action <p>The view controller holds its delegate weakly, so retain the delegate in your app. SwiftUI receives the same events through the <code>StorytellerClipsView</code> action closure shown above.</p>"},{"location":"EmbeddedClips/#safe-area-inset-management","title":"Safe Area / Inset Management","text":"<p>Constrain the UIKit view controller or SwiftUI wrapper to a non-zero area that respects the host safe area. This prevents Player controls from being obscured by navigation, status, or tab bars.</p>"},{"location":"EmbeddedClips/#continue-your-integration","title":"Continue Your Integration","text":"<ul> <li>See Custom Themes for Embedded Clips appearance and spacing.</li> <li>Add <code>context</code> to the shared collection configuration for Analytics attribution.</li> <li>See Ads before enabling opening pre-roll or bottom banner placements.</li> </ul>"},{"location":"MigrationGuideV11/","title":"Migrating to version 11","text":"<p>This guide helps you migrate from Storyteller SDK version 10.x.x to 11.0.x. Version 11.0.x introduces several breaking changes that improve the SDK's architecture and consistency. Version 11 focuses on three major architectural improvements:</p> <ul> <li>Shared Instance Pattern - Static methods replaced with <code>Storyteller.shared</code></li> <li>Consistent Naming - Public types now use <code>Storyteller</code> prefix</li> <li>Modern Swift Concurrency - Callback-based APIs replaced with async/await</li> </ul>"},{"location":"MigrationGuideV11/#storyteller-shared-instance","title":"Storyteller shared instance","text":"<p>The <code>Storyteller</code> class has moved from using static methods to a shared instance pattern. All API calls must now use <code>Storyteller.shared</code> instead of calling static methods directly on the class.</p>"},{"location":"MigrationGuideV11/#usage-example","title":"Usage example","text":"<p>Before (10.x.x):</p> <pre><code>Storyteller.delegate = myDelegate\n</code></pre> <p>After (11.x.x):</p> <pre><code>final class DelegateObject: StorytellerDelegate {}\nlet myDelegate = DelegateObject()\nStoryteller.shared.delegate = myDelegate\n</code></pre> <p>See the shared instance pattern in the Showcase app where <code>Storyteller.shared.modules</code>, <code>Storyteller.shared.theme</code>, and <code>Storyteller.shared.delegate</code> are configured in <code>AppDelegate.setupStoryteller</code>.</p>"},{"location":"MigrationGuideV11/#storyteller-prefix-for-public-types","title":"Storyteller Prefix for Public Types","text":"<p>All public types now start with the <code>Storyteller</code> prefix for better namespace consistency and to avoid naming conflicts with your app code.</p>"},{"location":"MigrationGuideV11/#type-renames","title":"Type Renames","text":"Old Name New Name <code>UserInput</code> <code>StorytellerUserInput</code> <code>ClipCollectionConfiguration</code> <code>StorytellerClipCollectionConfiguration</code> <code>Placement</code> <code>StorytellerPlacement</code> <code>Category</code> <code>StorytellerCategory</code> <code>CategoryDetail</code> <code>StorytellerCategoryDetail</code> <code>CurrentCategoryData</code> <code>StorytellerCurrentCategoryData</code> <code>UserActivity</code> <code>StorytellerUserActivity</code> <code>UserActivityData</code> <code>StorytellerUserActivityData</code> <code>CodableIgnored</code> <code>StorytellerCodableIgnored</code> <code>Alignment</code> <code>StorytellerAlignment</code> <code>FontProvider</code> <code>StorytellerFontProvider</code> <code>TextCasing</code> <code>StorytellerTextCasing</code> <code>PlayerIcons</code> <code>StorytellerPlayerIcons</code> <code>InstructionIcons</code> <code>StorytellerInstructionIcons</code>"},{"location":"MigrationGuideV11/#asyncawait-functions","title":"Async/Await functions","text":"<p>All callback-based APIs have been replaced with modern Swift async/await patterns. The following <code>Storyteller.shared</code> methods are now async functions:</p> <ul> <li><code>initialize(apiKey:userInput:eventTrackingOptions:)</code></li> <li><code>dismissPlayer(animated:dismissReason:)</code></li> <li><code>openDeepLink(url:)</code></li> <li><code>openStory(id:openReason:)</code></li> <li><code>openStory(externalId:openReason:)</code></li> <li><code>openPage(id:openReason:)</code></li> <li><code>openCategory(category:openReason:)</code></li> <li><code>openCollection(configuration:openReason:)</code></li> <li><code>openClipByExternalId(collectionId:externalId:openReason:)</code></li> <li><code>openSheet(id:)</code></li> <li><code>getStoriesCount(for:)</code></li> <li><code>getClipsCount(for:)</code></li> <li><code>openSearch()</code></li> </ul>"},{"location":"MigrationGuideV11/#migration-examples","title":"Migration Examples","text":"<p>Before (10.x.x):</p> <pre><code>Storyteller.initialize(\n    apiKey: \"your-api-key\",\n    onComplete: {\n        print(\"SDK initialized successfully\")\n    },\n    onError: { error in\n        print(\"Initialization failed: \\(error)\")\n    }\n)\n</code></pre> <p>After (11.x.x):</p> <pre><code>Task {\n    do {\n        try await Storyteller.shared.initialize(apiKey: \"your-api-key\")\n        print(\"SDK initialized successfully\")\n    } catch {\n        print(\"Initialization failed: \\(error)\")\n    }\n}\n</code></pre> <p>See the Showcase initialization flow using async/await in <code>StorytellerService.setup</code>.</p>"},{"location":"MigrationGuideV11/#additional-breaking-changes","title":"Additional Breaking Changes","text":""},{"location":"MigrationGuideV11/#event-tracking-options","title":"Event Tracking Options","text":"<p><code>eventTrackingOptions</code> can now only be set during SDK initialization:</p> <p>Before (10.x.x):</p> <pre><code>// Initialize SDK\nStoryteller.initialize(\n    apiKey: \"your-api-key\",\n    onComplete: {\n        print(\"SDK initialized successfully\")\n    },\n    onError: { error in\n        print(\"Initialization failed: \\(error)\")\n    }\n)\n\n// Later in the code, modify tracking options\nStoryteller.eventTrackingOptions = StorytellerEventTrackingOptions(\n    enablePersonalization: true,\n    enableStorytellerTracking: true,\n    enableUserActivityTracking: true,\n    enableAdTracking: true,\n    enableFullVideoAnalytics: true,\n    enableRemoteViewingStore: true,\n    disabledFunctionalFeatures: []\n)\n</code></pre> <p>After (11.x.x):</p> <pre><code>// Set tracking options during initialization\nlet trackingOptions = StorytellerEventTrackingOptions(\n    enablePersonalization: true,\n    enableStorytellerTracking: true,\n    enableUserActivityTracking: true,\n    enableAdTracking: true,\n    enableFullVideoAnalytics: true,\n    enableRemoteViewingStore: true,\n    disabledFunctionalFeatures: []\n)\n\nlet userInput = StorytellerUserInput(externalId: \"user-id\")\n\nTask {\n    try await Storyteller.shared.initialize(\n        apiKey: \"your-api-key\",\n        userInput: userInput,\n        eventTrackingOptions: trackingOptions\n    )\n}\n</code></pre> <p>See how the Showcase app builds <code>StorytellerEventTrackingOptions</code> and passes them during initialization in <code>StorytellerService.setup</code>.</p> <p>To change tracking options after initialization, you must reinitialize the SDK. See Privacy and Tracking for more information.</p>"},{"location":"MigrationGuideV11/#swiftui-grids","title":"SwiftUI Grids","text":"<p>The <code>isScrollable</code> parameter no longer has a default value and must be explicitly provided:</p> <p>Before (10.x.x):</p> <pre><code>// isScrollable defaulted to false\nStorytellerStoriesGrid(model: storiesModel)\nStorytellerClipsGrid(model: clipsModel)\n</code></pre> <p>After (11.x.x):</p> <pre><code>// isScrollable must be explicitly provided\nlet storiesModel = StorytellerStoriesListModel(\n    configuration: StorytellerStoriesListConfiguration(categories: [\"category-id\"])\n)\nlet clipsModel = StorytellerClipsListModel(\n    configuration: StorytellerClipsListConfiguration(collectionId: \"collection-id\")\n)\nStorytellerStoriesGrid(isScrollable: false, model: storiesModel)\nStorytellerClipsGrid(isScrollable: false, model: clipsModel)\n</code></pre> <p>See the Showcase SwiftUI grid usage in <code>StoriesListView</code>.</p>"},{"location":"MigrationGuideV11/#storytellerlistviewdelegate","title":"StorytellerListViewDelegate","text":"<p>The <code>onTileTapped</code> method now provides richer context via the <code>StorytellerTileType</code> enum:</p> <p>Before (10.x.x):</p> <pre><code>extension MyViewController: StorytellerListViewDelegate {\n    func onTileTapped(id: String) {\n        print(\"Tapped tile with ID: \\(id)\")\n    }\n}\n</code></pre> <p>After (11.x.x):</p> <pre><code>final class MyViewController: UIViewController, StorytellerListViewDelegate {\n    nonisolated func onTileTapped(type: StorytellerTileType) {\n        switch type {\n        case .clip(let clipId, let collectionId, let categories):\n            print(\"Tapped clip: \\(clipId) in collection: \\(collectionId), categories: \\(categories)\")\n        case .story(let storyId, let categories):\n            print(\"Tapped story: \\(storyId), categories: \\(categories)\")\n        @unknown default:\n            break\n        }\n    }\n}\n</code></pre> <p>See the Showcase <code>onTileTapped</code> flow (including categories) in <code>StorytellerItemView.listAction</code>.</p>"},{"location":"MigrationGuideV11/#other-breaking-changes","title":"Other breaking changes","text":"<p>The following theme properties have been removed and are now configured in the CMS:</p> <ul> <li><code>tiles.title.show</code> - configured in CMS</li> <li><code>engagement.poll.showVoteCount</code> - configured in CMS</li> </ul>"},{"location":"MigrationGuideV11/#need-help","title":"Need Help","text":"<p>If you encounter any issues during the migration:</p> <ol> <li>Check the Changelog for a detailed version history</li> <li>Don't hesitate to reach out if you continue to face difficulties</li> </ol>"},{"location":"NavigatingToApp/","title":"Navigating to App","text":"<p>A <code>StorytellerDelegate</code> has a method for managing in app navigation. It can be used for custom handling of <code>deeplink</code> URLs which can configured per page in CMS. This method will be called when user presses on the action button with the <code>deeplink</code> navigation type.</p>"},{"location":"NavigatingToApp/#how-to-use","title":"How to Use","text":"<p>To use global <code>userNavigatedToApp</code>, implement the <code>StorytellerDelegate</code> interface by overriding the required methods and set it in <code>Storyteller</code> object. <code>userNavigatedToApp(url: String)</code> is the method responsible for handling these <code>deeplink</code> URLs.</p> <p>The Showcase app wires this end-to-end: <code>userNavigatedToApp</code> is implemented in <code>StorytellerInstanceDelegate.userNavigatedToApp</code> and routes to <code>Router.navigateToActionLink</code>.</p>"},{"location":"NavigatingToApp/#navigation-to-the-integrating-app","title":"Navigation to the Integrating App","text":"<p>The callback <code>userNavigatedToApp</code> is called when a user taps on an action button on a page which has its link type set to <code>deeplink</code>. In this case, your app will be passed a URL which has been entered in the Storyteller CMS and your app is then responsible for parsing this URL and following it to the correct location within your app.</p> <p>Example:</p> <pre><code>func userNavigatedToApp(url: String) {\n    // parse the url and navigate to the destination\n}\n</code></pre>"},{"location":"OpenPlayer/","title":"Open Player","text":"<p>These methods allow you to programmatically open the Storyteller player to specific content like Stories, Pages, Categories, or Clip Collections.</p> <p>All methods on this page are asynchronous and throwing. Call them from an asynchronous context and handle failures. For example:</p> <pre><code>import StorytellerSDK\n\nfunc openFeaturedStory() {\n    Task { @MainActor in\n        do {\n            try await Storyteller.shared.openStory(id: \"featured-story\")\n        } catch {\n            print(\"Unable to open Story: \\(error.localizedDescription)\")\n        }\n    }\n}\n</code></pre>"},{"location":"OpenPlayer/#opening-stories-categories","title":"Opening Stories &amp; Categories","text":""},{"location":"OpenPlayer/#opencategory","title":"openCategory","text":"<p>Opens a list of Stories filtered by a specific category ID.</p> <pre><code>try await Storyteller.shared.openCategory(category: \"category-id\")\n</code></pre> <p>See the Showcase usage of <code>openCategory</code> in <code>FeedImageView</code>.</p> <p>Parameters:</p> <ul> <li><code>category</code>: The ID of the Story category to open.</li> <li><code>openReason</code>: Why the player was opened. Used only for analytics.</li> </ul> <p>Throws if there is an issue opening the category (e.g., the category is not available).</p>"},{"location":"OpenPlayer/#openstory-by-id","title":"openStory (by ID)","text":"<p>Opens a single Story by its specific ID.</p> <pre><code>try await Storyteller.shared.openStory(id: \"story-id\")\n</code></pre> <p>Parameters:</p> <ul> <li><code>id</code>: The ID of the Story to open.</li> <li><code>openReason</code>: Why the player was opened. Used only for analytics.</li> </ul> <p>Throws if there is an issue opening the Story (e.g., the requested Story is not available).</p>"},{"location":"OpenPlayer/#openstory-by-external-id","title":"openStory (by External ID)","text":"<p>Opens a single Story by its assigned external ID.</p> <pre><code>try await Storyteller.shared.openStory(externalId: \"story-external-id\")\n</code></pre> <p>Parameters:</p> <ul> <li><code>externalId</code>: The external ID of the Story to open.</li> <li><code>openReason</code>: Why the player was opened. Used only for analytics.</li> </ul> <p>Throws if there is an issue opening the Story (e.g., no Story found with the external ID).</p>"},{"location":"OpenPlayer/#openpage","title":"openPage","text":"<p>Opens a specific Page within its Story. The SDK deduces the correct Story based on the Page ID.</p> <pre><code>try await Storyteller.shared.openPage(id: \"page-id\")\n</code></pre> <p>Parameters:</p> <ul> <li><code>id</code>: The ID of the Page to open.</li> <li><code>openReason</code>: Why the player was opened. Used only for analytics.</li> </ul> <p>Throws if there is an issue opening the Page (e.g., the requested Page is not available).</p>"},{"location":"OpenPlayer/#opening-clips-collections","title":"Opening Clips &amp; Collections","text":""},{"location":"OpenPlayer/#opencollection","title":"openCollection","text":"<p>Opens a collection of Clips, optionally starting at a specific Clip or Category.</p> <pre><code>let configuration = StorytellerClipCollectionConfiguration(collectionId: \"collection-id\")\ntry await Storyteller.shared.openCollection(configuration: configuration)\n</code></pre> <p>Parameters:</p> <ul> <li><code>configuration</code>: A <code>StorytellerClipCollectionConfiguration</code> struct specifying the collection and optional starting points (see Embedded Clips for configuration details).</li> </ul> <p>If a specific clip or category is specified via <code>configuration.destination</code> and found, it will be opened. Otherwise, the first clip in the collection is opened.</p> <p>Throws if there is an issue opening the Collection (e.g., the requested Collection is not available).</p> <pre><code>import StorytellerSDK\n\nfunc openTopPlays() {\n    let configuration = StorytellerClipCollectionConfiguration(\n        collectionId: \"top-plays\",\n        context: [\"location\": \"home\"]\n    )\n\n    Task { @MainActor in\n        do {\n            try await Storyteller.shared.openCollection(configuration: configuration)\n        } catch {\n            print(\"Unable to open Clips: \\(error.localizedDescription)\")\n        }\n    }\n}\n</code></pre>"},{"location":"OpenPlayer/#open-reason-enum","title":"Open Reason Enum","text":"<p>The optional <code>openReason</code> parameter accepts values of <code>StorytellerOpenReason</code>:</p> <pre><code>let openReason: StorytellerOpenReason = .instanceMethod\n</code></pre> <p>This value is used only for analytics and has no functional effect on how the player behaves.</p>"},{"location":"OpenPlayer/#openclipbyexternalid","title":"openClipByExternalId","text":"<p>Opens a collection of Clips and attempts to navigate directly to a specific Clip within that collection using its external ID.</p> <pre><code>try await Storyteller.shared.openClipByExternalId(\n    collectionId: \"collection-id\",\n    externalId: \"clip-external-id\"\n)\n</code></pre> <p>Parameters:</p> <ul> <li><code>collectionId</code>: The ID of the Clip Collection.</li> <li><code>externalId</code>: The external ID of the specific Clip to open.</li> <li><code>openReason</code>: Why the player was opened. Used only for analytics.</li> </ul> <p>If the clip with the <code>externalId</code> is found within the collection, it will be opened. Otherwise, the player will open to the first clip in the collection.</p> <p>Throws if there is an issue opening the Collection (e.g., the requested Collection is not available).</p>"},{"location":"PrivacyAndTracking/","title":"Privacy and Tracking","text":"<p>The <code>eventTrackingOptions</code> property customizes Storyteller's analytics and tracking behavior. It allows certain features to be disabled based on user privacy choices. It is an object of type <code>StorytellerEventTrackingOptions</code> and by default, all of its properties are enabled.</p> <p>To receive and forward events in your own analytics system, follow Integrate Analytics. This page defines the privacy and tracking controls used by that setup.</p> <p>Important: <code>eventTrackingOptions</code> can only be set during SDK initialization. To configure tracking options, pass a <code>StorytellerEventTrackingOptions</code> object to the <code>initialize</code> method. The default value is <code>.enableAll</code>:</p> <pre><code>// Using custom tracking options\nlet trackingOptions = StorytellerEventTrackingOptions(\n    enablePersonalization: false,\n    enableStorytellerTracking: false,\n    enableUserActivityTracking: false,\n    enableAdTracking: false,\n    enableFullVideoAnalytics: false,\n    enableRemoteViewingStore: false,\n    disabledFunctionalFeatures: []\n)\n\ntry await Storyteller.shared.initialize(\n    apiKey: \"your-api-key\",\n    userInput: StorytellerUserInput(externalId: \"user-id\"),\n    eventTrackingOptions: trackingOptions\n)\n\n// Or use the default .enableAll (all tracking enabled)\ntry await Storyteller.shared.initialize(\n    apiKey: \"your-api-key\",\n    userInput: StorytellerUserInput(externalId: \"user-id\")\n)\n</code></pre> <p>For a full integration, see how the Showcase app builds <code>StorytellerEventTrackingOptions</code> and initializes the SDK in <code>StorytellerService.setup</code>.</p> <p>The property remains publicly readable via <code>Storyteller.shared.eventTrackingOptions</code>, but can no longer be modified at runtime. To change tracking options after initialization, you must reinitialize the SDK with new options.</p> <p>If an expected event or Ad callback is missing, follow Callbacks or Analytics Events Do Not Arrive before changing privacy settings; data loading, user-activity delivery, and Ad analytics use different callback paths.</p>"},{"location":"PrivacyAndTracking/#user-personalization","title":"User Personalization","text":"<p>When <code>enablePersonalization</code> is enabled, supported Storyteller requests include user attributes so the returned content can be personalized. The standard <code>userId</code> query parameter is included on supported personalization requests only when both <code>enablePersonalization</code> and <code>enableRemoteViewingStore</code> are enabled.</p> <p><code>enableRemoteViewingStore</code> also controls separate requests used to retrieve and record viewing activity and synchronize followed categories. When it is enabled, those requests can include the stored hashed user ID even when <code>enablePersonalization</code> is disabled.</p>"},{"location":"PrivacyAndTracking/#storyteller-tracking","title":"Storyteller tracking","text":"<p>When <code>enableStorytellerTracking</code> is enabled, we will record analytics events on our servers. Note that some events are necessary for user functionality and will still be transmitted (but not stored) even when this setting is off.</p>"},{"location":"PrivacyAndTracking/#user-activity-tracking","title":"User Activity tracking","text":"<p>When <code>enableUserActivityTracking</code> is enabled, we call the Storyteller delegate's method <code>onUserActivityOccurred()</code>, which allows integrating apps to record our analytics events on their own systems. When it is disabled, the callback is not invoked. Ad loading callbacks such as <code>getAd</code> and <code>getBottomBannerAd</code> are separate integration APIs and do not change that user-activity callback gate.</p>"},{"location":"PrivacyAndTracking/#ads-tracking","title":"Ads tracking","text":"<p>When <code>enableAdTracking</code> is disabled, ad-related events will not be tracked through <code>onUserActivityOccurred()</code> Storyteller delegate method and on our servers. Additionally, only necessary fields like Ad Unit Id and Custom Template Id's will be included in GAM requests.</p>"},{"location":"PrivacyAndTracking/#videos-tracking","title":"Videos tracking","text":"<p>When <code>enableFullVideoAnalytics</code> is disabled, sensitive video event data for <code>Story ID</code>, <code>Page ID</code>, <code>Story Title</code>, <code>Page Title</code>, <code>Clip ID</code>, <code>Clip Title</code>, <code>Story Display Title</code>, <code>Item Title</code>, <code>Container Title</code>, <code>Card Id</code>, <code>Card Title</code> and <code>Card Subtitle</code> will not be included in the <code>onUserActivityOccurred()</code> Storyteller delegate method.</p>"},{"location":"PrivacyAndTracking/#remote-viewing-store","title":"Remote viewing store","text":"<p>When <code>enableRemoteViewingStore</code> is disabled, user IDs are never stored or sent to backend services, and all user viewing activity is only kept locally on the device. This mode ensures the SDK operates in a privacy-enhanced mode designed to address VPPA (Video Privacy Protection Act) compliance concerns.</p>"},{"location":"PrivacyAndTracking/#independent-functional-behavior-toggles","title":"Independent Functional Behavior Toggles","text":"<p>The <code>disabledFunctionalFeatures</code> property allows to conditionally disable functional features of the SDK for privacy compliance. When a feature is disabled, the SDK will behave as if that functionality is disabled from the server.</p>"},{"location":"PrivacyAndTracking/#pagereadstatus","title":"<code>pageReadStatus</code>","text":"<p>Summary: Controls whether read/unread status is enabled for Story pages.</p> <p>When disabled, the SDK will not store read/unread behavior for pages. All Lists and Players will act as if read/unread tracking is disabled from the server, meaning users will not see visual indicators of which Stories they have previously viewed.</p>"},{"location":"PrivacyAndTracking/#clipviewedstatus","title":"<code>clipViewedStatus</code>","text":"<p>Summary: Controls whether viewed/not viewed status is enabled for individual Clips.</p> <p>When disabled, the SDK will not store viewed/not viewed information for Clips. All Lists and Players will act as if viewed/not viewed tracking is disabled from the server, removing visual indicators of previously watched content.</p>"},{"location":"PrivacyAndTracking/#pollvotes","title":"<code>pollVotes</code>","text":"<p>Summary: Controls whether Poll voting responses are persistently stored.</p> <p>When disabled, users can still interact with Polls and see immediate UI updates when they vote, but their votes are not persistently stored. If they navigate away and return to the same Poll, they can vote again. Since this is coupled with disabled Storyteller Analytics, their votes will not contribute to overall Poll statistics.</p>"},{"location":"PrivacyAndTracking/#triviaquizanswers","title":"<code>triviaQuizAnswers</code>","text":"<p>Summary: Controls whether Trivia Quiz responses and progress are persistently stored.</p> <p>When disabled, users can still answer trivia questions and see immediate UI feedback, but their answers are not persistently stored. If they navigate away and return to the same Quiz, they can answer questions again. The results page will be hidden since the SDK is not allowed to display persistent Quiz results.</p>"},{"location":"PrivacyAndTracking/#cliplikes","title":"<code>clipLikes</code>","text":"<p>Summary: Controls whether Clip like/unlike interactions are persistently stored.</p> <p>When disabled, users can still tap to like/unlike Clips and see immediate UI updates, but these interactions are not persistently stored. If they swipe away and return to the same Clip, it will appear in its original unliked state.</p>"},{"location":"PrivacyAndTracking/#clipshares","title":"<code>clipShares</code>","text":"<p>Summary: Disables Clip Share tracking and storage (but not the sharing action itself).</p> <p>When disabled, users can still tap on Share Clip button and see immediate UI updates, but this interaction is not persistently stored. If they swipe away and returns to the same Clip, the original share count will be displayed.</p>"},{"location":"PrivacyAndTracking/#all","title":"<code>all</code>","text":"<p>Summary: Disables all functional feature behaviors.</p> <p>When disabled using <code>.all</code>, the SDK will behave as if every individual functional toggle is turned off.</p>"},{"location":"Quickstart/","title":"iOS Quickstart Guide","text":"<p>This guide takes a new native iOS integration from an empty screen to a visible row of published Storyteller Stories. Choose either the complete UIKit or SwiftUI path below, then use the Showcase app and component guides for more advanced integrations.</p> <p>For Apple TV, use the dedicated tvOS Guide.</p>"},{"location":"Quickstart/#before-you-start","title":"Before You Start","text":"<p>You need:</p> <ul> <li>An iOS deployment target of 13.0 or later. We test the current SDK line with Xcode 26.2; earlier Xcode versions may also work, but they are not part of our tested configuration. The example apps have separate Showcase build requirements.</li> <li>A Storyteller API key for the tenant and environment you are integrating. Ask your Storyteller contact if you do not have one.</li> <li>A Story category identifier from the same tenant and environment. Ask your Storyteller contact which category should power your first row if this is not already part of your content setup.</li> <li>At least one Story assigned to that category and published in the Storyteller CMS. Draft, scheduled, expired, or otherwise unavailable content will not appear.</li> <li>A stable, non-personally-identifiable ID for the current user. The examples use <code>&lt;USER_ID&gt;</code>; see Working with Users before choosing a production identifier.</li> <li>If you plan to use the linked implementation examples, an authorised GitHub account with access to the private iOS Showcase source.</li> </ul> <p>Keep the API key, category identifier, and published content in the same tenant and environment. A valid key cannot load a category from another tenant.</p>"},{"location":"Quickstart/#install-the-sdk","title":"Install the SDK","text":""},{"location":"Quickstart/#swift-package-manager-recommended","title":"Swift Package Manager (Recommended)","text":"<ol> <li>In Xcode, open File &gt; Add Packages\u2026.</li> <li>Enter <code>https://github.com/getstoryteller/storyteller-sdk-swift-package</code>.</li> <li>Add the <code>StorytellerSDK</code> product to your app target.</li> </ol> <p>The package resolves <code>StorytellerLottie</code> 4.6.0 transitively, so you do not need to add <code>StorytellerLottie</code> separately. If your app also uses upstream Airbnb Lottie, keep it as its own dependency; its <code>Lottie</code> module can coexist with Storyteller's namespaced <code>StorytellerLottie</code> module.</p>"},{"location":"Quickstart/#cocoapods","title":"CocoaPods","text":"<p>Add the Storyteller sources and SDK to your <code>Podfile</code>:</p> <pre><code>source 'https://github.com/getstoryteller/storyteller-sdk-ios-podspec.git'\nsource 'https://github.com/getstoryteller/storyteller-lottie-ios-podspec.git'\nsource 'https://cdn.cocoapods.org/'\n\nuse_frameworks!\n\ntarget 'MyAwesomeApp' do\n  pod 'StorytellerSDK'\nend\n</code></pre> <p>Both Storyteller source lines are required: the first contains the SDK podspec and the second contains its <code>StorytellerLottie</code> dependency. The CocoaPods CDN supplies public dependencies.</p> <p>Then run <code>pod install</code> and open the generated <code>.xcworkspace</code>, not the <code>.xcodeproj</code>. CocoaPods installs <code>StorytellerLottie</code> transitively, so you do not need to add it separately. If your app also uses upstream Airbnb Lottie, keep it as its own dependency; it can coexist with <code>StorytellerLottie</code>.</p>"},{"location":"Quickstart/#xcframeworks","title":"XCFrameworks","text":"<ol> <li>Download and unzip <code>StorytellerSDK</code> 11.6.0.</li> <li>Download and unzip <code>StorytellerLottie</code> 4.6.0.</li> <li>Add <code>StorytellerSDK.xcframework</code> and <code>StorytellerLottie.xcframework</code> to the same app target. <code>StorytellerLottie</code> is required by the SDK. If your app also uses upstream Airbnb Lottie, include it separately; it can coexist with <code>StorytellerLottie</code>, but it does not replace it.</li> <li>In the app target's General &gt; Frameworks, Libraries, and Embedded Content section, select Embed &amp; Sign for both XCFrameworks.</li> </ol>"},{"location":"Quickstart/#add-your-first-stories-row","title":"Add Your First Stories Row","text":"<p>Replace these placeholders before running either example:</p> <ul> <li><code>&lt;API_KEY&gt;</code>: the API key supplied for your Storyteller tenant and environment.</li> <li><code>&lt;USER_ID&gt;</code>: a stable, non-personally-identifiable identifier for the current user.</li> <li><code>&lt;CATEGORY_ID&gt;</code>: a category that contains at least one currently published Story.</li> </ul> <p>Both examples deliberately finish <code>Storyteller.shared.initialize(...)</code> before configuring and loading the row. They also expose initialization, loading, empty, failure, and success states so a blank screen is not the only diagnostic signal.</p>"},{"location":"Quickstart/#uikit","title":"UIKit","text":"<p>Use this view controller directly or copy the same initialization, constraints, and delegate handling into your own screen.</p> <pre><code>import StorytellerSDK\nimport UIKit\n\n// MARK: - UIKitQuickstartViewController\n\nfinal class UIKitQuickstartViewController: UIViewController {\n    // MARK: Internal\n\n    override func viewDidLoad() {\n        super.viewDidLoad()\n\n        configureLayout()\n\n        Task { [weak self] in\n            await self?.loadStories()\n        }\n    }\n\n    // MARK: Private\n\n    private let statusLabel = UILabel()\n    private let storiesRow = StorytellerStoriesRowView()\n\n    private func configureLayout() {\n        view.backgroundColor = .systemBackground\n\n        statusLabel.numberOfLines = 0\n        statusLabel.text = \"Initializing Storyteller\u2026\"\n        statusLabel.translatesAutoresizingMaskIntoConstraints = false\n        storiesRow.translatesAutoresizingMaskIntoConstraints = false\n\n        view.addSubview(statusLabel)\n        view.addSubview(storiesRow)\n\n        NSLayoutConstraint.activate([\n            statusLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 24),\n            statusLabel.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor),\n            statusLabel.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor),\n\n            storiesRow.topAnchor.constraint(equalTo: statusLabel.bottomAnchor, constant: 16),\n            storiesRow.leadingAnchor.constraint(equalTo: view.leadingAnchor),\n            storiesRow.trailingAnchor.constraint(equalTo: view.trailingAnchor),\n            storiesRow.heightAnchor.constraint(equalToConstant: 180),\n            storiesRow.bottomAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.bottomAnchor),\n        ])\n    }\n\n    private func loadStories() async {\n        do {\n            try await Storyteller.shared.initialize(\n                apiKey: \"&lt;API_KEY&gt;\",\n                userInput: StorytellerUserInput(externalId: \"&lt;USER_ID&gt;\")\n            )\n\n            storiesRow.delegate = self\n            storiesRow.configure(\n                with: StorytellerStoriesListConfiguration(\n                    categories: [\"&lt;CATEGORY_ID&gt;\"]\n                )\n            )\n            storiesRow.reloadData()\n        } catch {\n            statusLabel.isHidden = false\n            statusLabel.text = \"Storyteller could not initialize: \\(error.localizedDescription)\"\n        }\n    }\n}\n\n// MARK: StorytellerListViewDelegate\n\nextension UIKitQuickstartViewController: StorytellerListViewDelegate {\n    nonisolated func onDataLoadStarted() {\n        Task { @MainActor [weak self] in\n            self?.statusLabel.isHidden = false\n            self?.statusLabel.text = \"Loading Stories\u2026\"\n        }\n    }\n\n    nonisolated func onDataLoadComplete(success: Bool, error: Error?, dataCount: Int) {\n        let statusText: String?\n        if let error {\n            statusText = \"Stories could not load: \\(error.localizedDescription)\"\n        } else if !success {\n            statusText = \"Stories could not load. Check the Xcode console for details.\"\n        } else if dataCount == 0 {\n            statusText = \"No published Stories were found for this category.\"\n        } else {\n            statusText = nil\n        }\n\n        Task { @MainActor [weak self] in\n            self?.statusLabel.isHidden = statusText == nil\n            self?.statusLabel.text = statusText\n        }\n    }\n}\n</code></pre> <p>The explicit leading, trailing, top, and height constraints are required. Adding a <code>StorytellerStoriesRowView</code> to a hierarchy without giving it a non-zero layout can load data successfully while displaying nothing.</p>"},{"location":"Quickstart/#swiftui","title":"SwiftUI","text":"<p>Place <code>SwiftUIQuickstartView</code> anywhere in your SwiftUI hierarchy. The model is created only after initialization succeeds; inserting the row then triggers its initial load.</p> <pre><code>import StorytellerSDK\nimport SwiftUI\n\nstruct SwiftUIQuickstartView: View {\n    // MARK: Internal\n\n    var body: some View {\n        VStack(alignment: .leading, spacing: 16) {\n            statusView\n\n            if let storiesModel {\n                StorytellerStoriesRow(model: storiesModel) { action in\n                    Task { @MainActor in\n                        handle(action)\n                    }\n                }\n                .frame(height: 180)\n            }\n        }\n        .padding(.vertical, 24)\n        .onAppear {\n            Task { @MainActor in\n                await loadStories()\n            }\n        }\n    }\n\n    // MARK: Private\n\n    private enum LoadState {\n        case initializing\n        case loading\n        case empty\n        case ready\n        case failed(String)\n    }\n\n    @State private var hasStarted = false\n    @State private var loadState = LoadState.initializing\n    @State private var storiesModel: StorytellerStoriesListModel?\n\n    @ViewBuilder\n    private var statusView: some View {\n        switch loadState {\n        case .initializing:\n            Text(\"Initializing Storyteller\u2026\")\n        case .loading:\n            Text(\"Loading Stories\u2026\")\n        case .empty:\n            Text(\"No published Stories were found for this category.\")\n        case .ready:\n            EmptyView()\n        case .failed(let message):\n            Text(message)\n        }\n    }\n\n    @MainActor\n    private func loadStories() async {\n        guard !hasStarted else { return }\n        hasStarted = true\n\n        do {\n            try await Storyteller.shared.initialize(\n                apiKey: \"&lt;API_KEY&gt;\",\n                userInput: StorytellerUserInput(externalId: \"&lt;USER_ID&gt;\")\n            )\n\n            loadState = .loading\n            storiesModel = StorytellerStoriesListModel(\n                configuration: StorytellerStoriesListConfiguration(\n                    categories: [\"&lt;CATEGORY_ID&gt;\"]\n                )\n            )\n        } catch {\n            loadState = .failed(\n                \"Storyteller could not initialize: \\(error.localizedDescription)\"\n            )\n        }\n    }\n\n    @MainActor\n    private func handle(_ action: StorytellerListAction) {\n        switch action {\n        case .onDataLoadStarted:\n            loadState = .loading\n        case let .onDataLoadComplete(success, error, dataCount):\n            if let error {\n                loadState = .failed(\n                    \"Stories could not load: \\(error.localizedDescription)\"\n                )\n            } else if !success {\n                loadState = .failed(\n                    \"Stories could not load. Check the Xcode console for details.\"\n                )\n            } else if dataCount == 0 {\n                loadState = .empty\n            } else {\n                loadState = .ready\n            }\n        default:\n            break\n        }\n    }\n}\n</code></pre> <p>Keep the explicit <code>.frame(height: 180)</code> or give the row an equivalent non-zero height within your own layout.</p>"},{"location":"Quickstart/#initialization-order","title":"Initialization Order","text":"<p><code>Storyteller.shared.initialize(...)</code> is <code>async throws</code>. Its <code>isInitialized</code> value becomes <code>true</code> only after initialization succeeds and is reset to <code>false</code> whenever initialization starts again. Across Storyteller's public methods, the stable <code>StorytellerError</code> cases are <code>networkError(Error)</code>, <code>contentNotFound(String)</code>, and <code>wrongInputData</code>. Initialization may also surface an underlying transport or response-decoding error, so keep a general <code>catch</code> path.</p> <p>The SDK currently defers a list reload requested before initialization. If an initialization attempt fails, the reload remains pending and can resume after a later successful attempt, provided the list identifier has not changed. This is a safeguard, not the recommended integration sequence. Await initialization explicitly as shown above.</p>"},{"location":"Quickstart/#confirm-success","title":"Confirm Success","text":"<p>A successful load calls <code>onDataLoadComplete</code> with <code>success == true</code>, no error, and <code>dataCount &gt; 0</code>. The status text disappears and a horizontal row of Story tiles appears. Tapping a tile opens the Story Player unless your theme disables Player opening.</p> <p>The tile artwork and shape depend on your tenant content and theme, but the populated row will look similar to the Story tiles in this visual:</p> <p></p>"},{"location":"Quickstart/#diagnose-a-blank-or-failed-result","title":"Diagnose a Blank or Failed Result","text":"Observable result What it establishes What to check <code>Storyteller could not initialize</code> The content request did not start. Confirm the API key, tenant/environment, connectivity, and the underlying error in Xcode. <code>Stories could not load</code> Initialization succeeded, but the content request failed. Inspect the returned error and Xcode logs; verify network access and tenant configuration. <code>No published Stories were found</code> The request succeeded with <code>dataCount == 0</code>. Confirm <code>&lt;CATEGORY_ID&gt;</code>, category assignment, publication status, schedule/expiry, and any targeting rules for <code>&lt;USER_ID&gt;</code>. <code>success == true</code> and <code>dataCount &gt; 0</code>, but no tiles are visible Content loaded and the problem is presentation. Confirm that the UIKit row has non-zero constraints or that the SwiftUI row has a non-zero frame. The screen remains in a loading state A request has not completed. Confirm initialization is awaited, keep the delegate/action callback alive, inspect Xcode logs, and check connectivity."},{"location":"Quickstart/#go-further","title":"Go Further","text":"<p>The Quickstart intentionally stops after the first reliable row. Continue with:</p> <ul> <li>Troubleshooting Storyteller on iOS when you need to distinguish app integration, content, tenant, provider, and SDK causes from an observable symptom.</li> <li>Storyteller List Views for grids, Clips rows, display limits, themes, and analytics context.</li> <li>Working with Users for external IDs, personalization, locale, and followed categories.</li> <li>Privacy and Tracking before selecting production tracking options.</li> <li>Custom Themes for tile and Player styling.</li> <li>The Showcase app's SwiftUI list model, SwiftUI action handling, and UIKit list example for richer, production-shaped examples.</li> </ul>"},{"location":"Search/","title":"Search","text":"<p>The <code>Search</code> component allows users to search for Storyteller Clips and Stories. As users type, a list of suggestions will appear, from which users can either select a suggestion or search using their entered term. Results are categorized into two sections: <code>Stories</code> and <code>Clips</code>, based on their type.  Filters allow users to narrow down their search results, enabling them to find specific content that meets their criteria more efficiently. For instance, users can apply filters such as date posted, content type or they can sort it by certain criteria.</p>"},{"location":"Search/#search-filters","title":"Search Filters","text":"<p><code>Date Posted</code> possible values:</p> <ul> <li><code>All</code> - default value</li> <li><code>Past 24 hours</code></li> <li><code>Last Week</code></li> <li><code>Last Month</code></li> <li><code>Last Year</code></li> </ul> <p><code>Content Type</code> possible values:</p> <ul> <li><code>All</code> - default value</li> <li><code>Stories</code></li> <li><code>Clips</code></li> </ul> <p><code>Sort By</code> values:</p> <ul> <li><code>Relevance</code> - default value</li> <li><code>Like Count</code></li> <li><code>Share Count</code></li> <li><code>Date Posted</code></li> </ul>"},{"location":"Search/#how-to-use","title":"How to Use","text":"<p>The <code>Search</code> feature must first be enabled by the Storyteller team for your specific tenant. Once enabled, the Search functionality will be available within the Story and Clip players.</p> <p>Additional <code>Search</code> functionality is available through the <code>Storyteller</code> class:</p> <ul> <li><code>isSearchEnabled</code> - returns whether search functionality is enabled at the app level.</li> <li><code>openSearch</code> - opens the <code>Search</code> component from anywhere in the app. If a Storyteller Player is currently displayed, it will be dismissed before presenting Search.</li> </ul> <p><code>openSearch()</code> is asynchronous and non-throwing. When Search is disabled, it returns without presenting anything, so check <code>isSearchEnabled</code> first if your app needs to show a disabled state or diagnostic:</p> <pre><code>import StorytellerSDK\n\nfunc searchButtonTapped() {\n    Task { @MainActor in\n        guard Storyteller.shared.isSearchEnabled else {\n            print(\"Storyteller Search is not enabled for this tenant\")\n            return\n        }\n\n        await Storyteller.shared.openSearch()\n    }\n}\n</code></pre> <p>The Showcase app triggers <code>openSearch</code> from the home header button - see <code>HomeView.headerView</code>.</p>"},{"location":"Search/#customization","title":"Customization","text":"<p>The Search background, input, filter button, suggestions, no-results state and filter sheet can be customized independently for light and dark themes. All appearance properties are optional and retain the existing UI when omitted. For the full property contract and inheritance rules, see Search themes.</p>"},{"location":"StorytellerDelegate/","title":"Implementing Storyteller Delegate Callbacks","text":"<p>Storyteller has a framework-independent global delegate and framework-specific component callback routes:</p> What you need to observe UIKit SwiftUI Player lifecycle, analytics, Ads, sharing, logging, or in-app navigation <code>StorytellerDelegate</code> assigned to <code>Storyteller.shared.delegate</code> The same <code>StorytellerDelegate</code> Story or Clip row/grid loading, taps, or Player dismissal <code>StorytellerListViewDelegate</code> assigned to the UIKit view <code>StorytellerListActionCallback</code> passed to the SwiftUI wrapper Embedded Clips loading or top-level back navigation <code>StorytellerClipsViewControllerDelegate</code> <code>StorytellerClipsView</code> action closure; see Embedded Clips <p><code>StorytellerDelegate</code> is the global route for events and integration hooks associated with Story and Clip Players. It inherits from <code>StorytellerModule</code>, described in StorytellerModule. Component callbacks are separate and do not replace the global delegate.</p> <p>For a full implementation, see the Showcase delegate in <code>StorytellerInstanceDelegate</code> and the analytics forwarding in <code>StorytellerTrackingDelegate</code>.</p> <p>If an expected callback does not arrive, use the shared callback and analytics troubleshooting route to distinguish load callbacks, interaction callbacks, and analytics gates.</p>"},{"location":"StorytellerDelegate/#storytellerdelegate","title":"StorytellerDelegate","text":"<p>This protocol applies to both UIKit and SwiftUI apps. Assign one app-owned instance to <code>Storyteller.shared.delegate</code> and retain it strongly because the SDK property is weak. Implement only the optional callbacks your integration needs.</p>"},{"location":"StorytellerDelegate/#onuseractivityoccurred","title":"onUserActivityOccurred","text":"<p>The <code>onUserActivityOccurred(type: StorytellerUserActivity.EventType, data: StorytellerUserActivityData)</code> method is called when an analytics event is triggered within the SDK. This allows the integrating app to observe and potentially forward these events to their own analytics systems. Follow Integrate Analytics for setup and verification, then use the Analytics Event Reference for event types and data.</p>"},{"location":"StorytellerDelegate/#getad","title":"getAd","text":"<p>The <code>getAd(for adRequestInfo: StorytellerAdRequestInfo) async throws -&gt; StorytellerAd</code> method is called when the tenant is configured in the CMS to request full-screen ads from the integrating app. The app should fetch ad data asynchronously and return it directly to the SDK, or throw an error if no ad is available. See the Ads page for more details.</p>"},{"location":"StorytellerDelegate/#getbottombannerad","title":"getBottomBannerAd","text":"<p><code>getBottomBannerAd(for adRequestInfo: StorytellerAdRequestInfo, maxHeight: CGFloat) async throws -&gt; StorytellerAd</code> method is similar to <code>getAd</code>, but is called when the tenant is configured in the CMS to request bottom banner ads (displayed at the bottom of clips) from the integrating app. The <code>maxHeight</code> parameter indicates the maximum allowed height for the banner based on the current layout constraints. See the Ads page for more details.</p>"},{"location":"StorytellerDelegate/#usernavigatedtoapp","title":"userNavigatedToApp","text":"<p>The <code>userNavigatedToApp(url: String)</code> method is called when a user presses an action button on a page which should direct the user to a specific place within the integrating app. More information on <code>In App</code> links Navigating to App. For more information on deep linking, see the dedicated Deep linking page.</p>"},{"location":"StorytellerDelegate/#onsharebuttontapped","title":"onShareButtonTapped","text":"<p>The <code>onShareButtonTapped(text: String, title: String, url: String)</code> method is called when <code>Storyteller.shared.useCustomShareHandling</code> is set to <code>true</code> and a user taps the Share button in a Story or Clip. The SDK pauses the current Story or Clip, skips presenting the iOS share sheet, and forwards the same payload it would normally share so your app can present its own share flow.</p> <p>When <code>Storyteller.shared.useCustomShareHandling</code> remains <code>false</code> (the default), the SDK continues to present the native iOS share sheet and this callback is not invoked.</p> <p>When your custom share UI is dismissed, call <code>Storyteller.shared.resumePlayer()</code> to resume Storyteller playback.</p>"},{"location":"StorytellerDelegate/#configurewebview","title":"configureWebView","text":"<p>This method allows you to configure the WebView with custom settings or actions when the Storyteller SDK is about to display a WebView. This method is called before displaying WebView on the screen.</p> <p>It receives a <code>configuration</code> object which is a collection of properties that you use to initialize a web view.</p> <p>Note: <code>configureWebView</code> is available only on iOS.</p>"},{"location":"StorytellerDelegate/#categoryfollowactiontaken","title":"categoryFollowActionTaken","text":"<p>The method <code>categoryFollowActionTaken(category: StorytellerCategory, isFollowing: Bool)</code> is invoked when a user follows or unfollows a category of clips (the category can represent a player, a team etc.) from within the SDK's UI.</p> <p>The callback reports the affected category and whether it is now followed:</p> <ul> <li><code>category</code> - An object representing the clip category</li> <li><code>isFollowing</code> - A boolean value indicating whether the user is following or unfollowing the specified category</li> </ul> <p>Note: This method is only called when your tenant is setup in App-Managed Following mode.</p>"},{"location":"StorytellerDelegate/#log","title":"log","text":"<p>The <code>log(message:)</code> method receives Storyteller SDK error and informational log messages. Implement it on the app-owned object assigned to <code>Storyteller.shared.delegate</code> when you need to capture diagnostics in debug or release builds. The SDK holds the delegate weakly, so keep that object strongly retained. Failed-request messages can include full request URLs containing a hashed user ID when <code>enableRemoteViewingStore</code> is enabled, even when personalization is disabled; they can also include custom-attribute values when personalization is enabled. Redact those values before forwarding logs to a third-party service or sharing them. Storyteller API keys can remain intact when sharing logs directly with Storyteller support.</p>"},{"location":"StorytellerDelegate/#onplayerpresented","title":"onPlayerPresented","text":"<p>The method <code>onPlayerPresented()</code> is invoked when a story or clip player is presented on screen. This can be useful for pausing background audio, videos or animations in your app while the player is visible.</p>"},{"location":"StorytellerDelegate/#onplayerdismissed","title":"onPlayerDismissed","text":"<p>The method <code>onPlayerDismissed()</code> is invoked when a story or clip player is dismissed from screen. This can be useful for resuming background audio, videos or animations in your app that were paused when the player was presented.</p>"},{"location":"StorytellerDelegate/#viewcontrollerfor-storytellercategory","title":"viewController(for: StorytellerCategory)","text":"<p>The method <code>viewController(for category: StorytellerCategory) -&gt; UIViewController?</code> is invoked when the user taps on the category icon inside a clip or interactively swipes to the left. You can provide a custom view controller to push to. This method is optional, and if an implementation is not provided, the SDK will push a <code>UIViewController</code> with a Story Row and Clip Grid based on the Category.</p> <p>This delegate method receives the following parameter:</p> <ul> <li><code>category</code> - An object representing the clip category</li> </ul>"},{"location":"StorytellerDelegate/#minimal-storytellerdelegate-implementation","title":"Minimal <code>StorytellerDelegate</code> implementation","text":"<p>Every <code>StorytellerDelegate</code> method has a default implementation, including the Ad methods inherited from <code>StorytellerModule</code>. Implement only the callbacks your app needs. This minimal analytics delegate is valid without placeholder return values:</p> <pre><code>import StorytellerSDK\n\nfinal class AnalyticsDelegate: StorytellerDelegate {\n    func onUserActivityOccurred(\n        type: StorytellerUserActivity.EventType,\n        data: StorytellerUserActivityData\n    ) {\n        print(\"Storyteller event: \\(type), context: \\(data.context ?? [:])\")\n    }\n}\n\nfinal class StorytellerIntegration {\n    private let delegate = AnalyticsDelegate()\n\n    func configure() {\n        Storyteller.shared.delegate = delegate\n    }\n}\n</code></pre> <p>Keep the delegate strongly referenced, as shown by <code>StorytellerIntegration</code>; <code>Storyteller.shared.delegate</code> is weak. Implement <code>getAd</code> or <code>getBottomBannerAd</code> only when your tenant requests host-supplied Ads, and return a real <code>StorytellerAd</code> or throw an error as described in Ads.</p> <p>WebKit types are not re-exported by StorytellerSDK. A delegate that customizes Storyteller WebViews must import <code>WebKit</code> explicitly:</p> <pre><code>import StorytellerSDK\nimport WebKit\n\nfinal class WebViewDelegate: StorytellerDelegate {\n    func configureWebView(configuration: inout WKWebViewConfiguration) {\n        MainActor.assumeIsolated {\n            let script = WKUserScript(\n                source: \"document.body.style.backgroundColor = 'red';\",\n                injectionTime: .atDocumentEnd,\n                forMainFrameOnly: true\n            )\n            configuration.userContentController.addUserScript(script)\n        }\n    }\n}\n</code></pre> <p>Storyteller invokes <code>configureWebView</code> while constructing UI on the main actor. The released protocol requirement predates WebKit's strict-concurrency annotations, so <code>MainActor.assumeIsolated</code> keeps host code warning-free on newer toolchains while preserving compatibility with the released SDK.</p>"},{"location":"StorytellerDelegate/#storytellerlistviewdelegate","title":"StorytellerListViewDelegate","text":"<p><code>StorytellerListViewDelegate</code> is the UIKit callback interface for <code>StorytellerRowView</code> and <code>StorytellerGridView</code> subclasses. SwiftUI list wrappers report the same event values through <code>StorytellerListActionCallback</code>; choose the corresponding wiring example below.</p>"},{"location":"StorytellerDelegate/#ondataloadstarted","title":"onDataLoadStarted","text":"<p>UIKit calls <code>onDataLoadStarted()</code> and SwiftUI emits <code>.onDataLoadStarted</code> when a list request begins.</p>"},{"location":"StorytellerDelegate/#ondataloadcomplete","title":"onDataLoadComplete","text":"<p>UIKit calls <code>onDataLoadComplete(success:error:dataCount:)</code> and SwiftUI emits <code>.onDataLoadComplete(success:error:dataCount:)</code> when the request finishes.</p> Property Description <code>success</code> This confirms whether or not the request was successful <code>error</code> The HTTP error if the request was not successful <code>dataCount</code> The number of Stories loaded"},{"location":"StorytellerDelegate/#ontiletapped","title":"onTileTapped","text":"<p>UIKit calls <code>onTileTapped(type:)</code> and SwiftUI emits <code>.onTileTapped(type:)</code> when a user taps a tile inside a row or grid. This happens before the Player is opened.</p> Property Description <code>type</code> A <code>StorytellerTileType</code> enum that contains tile information. Can be either <code>.story(storyId: String, categories: [StorytellerCategoryDetail])</code> or <code>.clip(clipId: String, collectionId: String, categories: [StorytellerCategoryDetail])</code> <p>Note: When <code>theme.lists.enablePlayerOpen</code> is set to <code>false</code>, the SDK will not automatically open the player and you should handle your custom tile interaction logic via this callback. For lists in SDK\u2011owned screens (Storyteller Home, Followable Categories, and Search), the SDK always opens the player when a tile is tapped, regardless of <code>theme.lists.enablePlayerOpen</code>.</p> <p>Example:</p> <pre><code>// Assuming `theme.lists.enablePlayerOpen` is set to `false`\nfunc onTileTapped(type: StorytellerTileType) {\n    switch type {\n    case .story(let storyId, let categories):\n        let categoryIds = categories.map(\\.id)\n        // Handle story tile tap\n    case .clip(let clipId, let collectionId, let categories):\n        let categoryIds = categories.map(\\.id)\n        // Handle clip tile tap\n    @unknown default:\n        break\n    }\n}\n</code></pre>"},{"location":"StorytellerDelegate/#onplayerdismissed_1","title":"<code>onPlayerDismissed</code>","text":"<p>UIKit calls <code>onPlayerDismissed()</code> and SwiftUI emits <code>.onPlayerDismissed</code> when a Player opened from the list is dismissed.</p>"},{"location":"StorytellerDelegate/#error-handling","title":"Error Handling","text":"<p>By using the callback function <code>onDataLoadComplete</code> and the data it provides, you can handle the current state of the <code>StorytellerRowView</code> appropriately in your app.</p> <p>Note: <code>dataCount</code> is the total number of Stories in the existing <code>StorytellerRowView</code> at any given time</p> <p>Example:</p> <pre><code>func onDataLoadComplete(success: Bool, error: Error?, dataCount: Int) {\n    if success {\n        // stories data has been loaded successfully\n        // dataCount is the current total number of content, including newly added/removed data\n    } else if let newError = error {\n        // an error has occurred, use the unwrapped value `newError`\n    }\n}\n</code></pre> <p>Another example:</p> <pre><code>let storytellerRowView = StorytellerStoriesRowView()\n\nfunc onDataLoadComplete(success: Bool, error: Error?, dataCount: Int) {\n    if let _ = error, dataCount == 0 {\n        // content have failed to load with error and there is no data to show\n        // you may wish to hide the `StorytellerRowView` instance here\n        storytellerRowView.isHidden = true\n        // Example: storytellerRowViewHeightConstraint.constant = 0\n    }\n}\n</code></pre>"},{"location":"StorytellerDelegate/#wire-list-callbacks","title":"Wire List Callbacks","text":"<p>Choose the callback route for your framework:</p> UIKitSwiftUI <p>Pass an action closure to the SwiftUI list wrapper and switch over <code>StorytellerListAction</code>:</p> <pre><code>import StorytellerSDK\nimport SwiftUI\n\n@available(iOS 14.0, *)\nstruct StoriesRowWithActions: View {\n    @State private var model = StorytellerStoriesListModel(\n        configuration: StorytellerStoriesListConfiguration(categories: [\"sports\"])\n    )\n\n    var body: some View {\n        StorytellerStoriesRow(model: model) { action in\n            switch action {\n            case .onDataLoadStarted:\n                print(\"Storyteller list started loading\")\n            case .onDataLoadComplete(let success, let error, let dataCount):\n                print(\"Loaded \\(dataCount) items; success: \\(success); error: \\(error?.localizedDescription ?? \"none\")\")\n            case .onTileTapped(let type):\n                print(\"Tapped Storyteller tile: \\(type)\")\n            case .onPlayerDismissed:\n                print(\"Storyteller Player dismissed\")\n            @unknown default:\n                break\n            }\n        }\n    }\n}\n</code></pre> <p>The wrapper owns the internal UIKit delegate bridge. Your SwiftUI code should consume the action closure rather than constructing a <code>StorytellerListViewDelegate</code>.</p> <p>See Storyteller List Views for complete list configuration and reload examples.</p>"},{"location":"StorytellerDelegate/#example-implementation-of-storytellerlistviewdelegate","title":"Example implementation of StorytellerListViewDelegate","text":"<p>Implement <code>StorytellerListViewDelegate</code>:</p> <pre><code>class DelegateObject : StorytellerListViewDelegate {\n\n    func onDataLoadStarted() {\n        // Action on start of data network requests\n    }\n\n    func onDataLoadComplete(success: Bool, error: Error?, dataCount: Int) {\n        // Action on completion of data network requests\n    }\n\n    func onTileTapped(type: StorytellerTileType) {\n        // Action when a tile is tapped\n    }\n\n    func onPlayerDismissed() {\n        // Action on dismissal of player\n    }\n}\n</code></pre> <p>Retain the delegate strongly, assign it to the UIKit view, and then load the content:</p> <pre><code>import StorytellerSDK\nimport UIKit\n\nfinal class StoriesListViewController: UIViewController {\n    private let storytellerStoriesRow = StorytellerStoriesRowView()\n    private let delegate = DelegateObject()\n\n    override func viewDidLoad() {\n        super.viewDidLoad()\n\n        storytellerStoriesRow.delegate = delegate\n        storytellerStoriesRow.reloadData()\n    }\n\n    private final class DelegateObject: StorytellerListViewDelegate {}\n}\n</code></pre> <p>Assign the delegate before <code>reloadData()</code> or the delegate will not receive the initial loading callbacks.</p>"},{"location":"StorytellerHome/","title":"Storyteller Home","text":"<p><code>StorytellerHome</code> is a component that allows multiple Stories and Clips Rows or Grids to be embedded in a single screen in your application in a list.</p>"},{"location":"StorytellerHome/#how-to-use","title":"How to Use","text":"<p>Create one shared configuration, then choose either the SwiftUI or UIKit implementation below. Both routes display the same CMS-configured Home content and support manual refresh through <code>reloadData</code>; pull-to-refresh is available in the SwiftUI component from iOS 15.</p>"},{"location":"StorytellerHome/#step-1-create-a-configuration-object","title":"Step 1: Create a configuration object","text":"<p>Begin by constructing a <code>StorytellerHomeConfiguration</code>. Pass this configuration to the model or view for the framework you use. It has the following properties:</p> <ul> <li><code>homeId</code> - an identifier of the home configuration you want to load.</li> <li><code>theme</code> - an optional theme parameter to be used for styling. If no theme is supplied, the theme set on the <code>Storyteller.shared.theme</code> property is used.</li> <li><code>uiStyle</code> - an optional parameter for overriding the appearance of the component. Possible values are <code>light</code>, <code>dark</code>, and <code>auto</code>. The default styling is <code>auto</code> and it uses the system setting value.</li> <li><code>context</code> - optional context data that will be included in analytics callbacks for attribution. This allows you to track which sources drive engagement with your home content. See Analytics for more details.</li> </ul> <pre><code>let theme = StorytellerTheme()\n// Customize the theme\nlet config = StorytellerHomeConfiguration(\n    homeId: \"YOUR_HOME_ID\",\n    theme: theme,\n    uiStyle: .auto,\n    context: [\n      \"source\": \"main-tab\",\n      \"user_segment\": \"premium\",\n      \"variant\": \"personalized\"\n  ]\n)\n</code></pre>"},{"location":"StorytellerHome/#choose-your-ui-framework","title":"Choose Your UI Framework","text":"<p>The framework routes are alternatives; follow only the tab that matches your app.</p> UIKitSwiftUI"},{"location":"StorytellerHome/#step-3-using-uikit","title":"Using UIKit","text":"<p>Create a <code>StorytellerHomeView</code> from the shared configuration and add it to a view hierarchy with non-zero constraints.</p> <p>For a UIKit sample that renders multiple Storyteller lists in a table view, see the CocoaPods Showcase implementation in <code>MultipleListsViewController</code>.</p> <pre><code>class MyViewController: UIViewController {\n  override func viewDidLoad() {\n    super.viewDidLoad()\n\n    let config = StorytellerHomeConfiguration(homeId: \"YOUR_HOME_ID\")\n    let storytellerHomeView = StorytellerHomeView(configuration: config)\n    storytellerHomeView.translatesAutoresizingMaskIntoConstraints = false\n    view.addSubview(storytellerHomeView)\n\n    // Set up constraints\n    NSLayoutConstraint.activate([\n        storytellerHomeView.topAnchor.constraint(equalTo: view.topAnchor),\n        storytellerHomeView.bottomAnchor.constraint(equalTo: view.bottomAnchor),\n        storytellerHomeView.leadingAnchor.constraint(equalTo: view.leadingAnchor),\n        storytellerHomeView.trailingAnchor.constraint(equalTo: view.trailingAnchor)\n    ])\n  }\n}\n</code></pre>"},{"location":"StorytellerHome/#step-2-using-swiftui","title":"Using SwiftUI","text":"<p>Create a <code>StorytellerHomeModel</code> from the shared configuration and retain it as view state. The <code>StorytellerHome</code> view includes built-in pull-to-refresh support from iOS 15.</p> <pre><code>@available(iOS 14.0, *)\nstruct ContentView: View {\n  // Initialize model with configuration\n  @StateObject private var model = StorytellerHomeModel(\n    configuration: StorytellerHomeConfiguration(homeId: \"YOUR_HOME_ID\")\n  )\n\n  var body: some View {\n    StorytellerHome(model: model)\n  }\n}\n</code></pre>"},{"location":"StorytellerHome/#continue-your-integration","title":"Continue Your Integration","text":"<ul> <li>Use Storyteller List Views when your app needs to compose individual Story or Clip rows and grids instead of a CMS-configured Home.</li> <li>See Custom Themes for Home appearance options.</li> <li>Add <code>context</code> to the shared configuration when you need Analytics attribution.</li> </ul>"},{"location":"StorytellerListViews/","title":"The Storyteller List Views","text":"<p>Use Storyteller List Views to embed a row or grid of Stories or Clips in your own UIKit or SwiftUI screen. If you have not displayed Storyteller content before, complete the iOS Quickstart Guide first.</p> <p>Choose the content, layout, and framework independently:</p> Content and layout UIKit SwiftUI Stories row <code>StorytellerStoriesRowView</code> <code>StorytellerStoriesRow</code> with <code>StorytellerStoriesListModel</code> Stories grid <code>StorytellerStoriesGridView</code> <code>StorytellerStoriesGrid</code> with <code>StorytellerStoriesListModel</code> Clips row <code>StorytellerClipsRowView</code> <code>StorytellerClipsRow</code> with <code>StorytellerClipsListModel</code> Clips grid <code>StorytellerClipsGridView</code> <code>StorytellerClipsGrid</code> with <code>StorytellerClipsListModel</code> <p>The <code>StorytellerStoriesListConfiguration</code> and <code>StorytellerClipsListConfiguration</code> values are shared by both frameworks. UIKit views report loading and interaction through <code>StorytellerListViewDelegate</code>; SwiftUI wrappers expose the equivalent events through <code>StorytellerListActionCallback</code>.</p>"},{"location":"StorytellerListViews/#uikit-list-types","title":"UIKit List Types","text":"<p>The four final UIKit List views have the following class hierarchy (root class is at the top):</p> <pre><code> StorytellerListView\n \u251c\u2500\u2500 StorytellerRowView\n \u2502   \u251c\u2500\u2500 StorytellerStoriesRowView\n \u2502   \u2514\u2500\u2500 StorytellerClipsRowView\n \u2514\u2500\u2500 StorytellerGridView\n     \u251c\u2500\u2500 StorytellerStoriesGridView\n     \u2514\u2500\u2500 StorytellerClipsGridView\n</code></pre> <p>Choose the one you need depending on content and UI behavior. Use a Stories or Clips list, then choose between these layouts:</p> <p>Rows are horizontal scrolling lists, that you create by simply calling:</p> <pre><code>let storiesRow = StorytellerStoriesRowView()\n</code></pre> <p>or</p> <pre><code>let clipsRow = StorytellerClipsRowView()\n</code></pre> <p>For a UIKit table view integration (including configuration and delegate wiring), see the CocoaPods sample in <code>StorytellerStoriesRowTableViewCell</code>.</p> <p>When using rows, you can either provide explicit height constraints or let the SDK manage the height automatically. When no explicit height constraint is set, the SDK will automatically calculate and adjust the row height, including adjustments for iOS Dynamic Type settings to improve accessibility.</p> <p>Grids are vertical lists, organized into columns (number of columns can be set on the Theme), and can be scrollable or not. They can be constructed like so:</p> <pre><code>let storiesGrid = StorytellerStoriesGridView(isScrollable: true) // scrollable\nlet clipsGrid = StorytellerClipsGridView(isScrollable: false) // non-scrollable\n</code></pre> <p>For a full SwiftUI Showcase Home feed that composes rows, grids, and Cards, see how the Showcase app assembles the feed in <code>HomeView</code>, renders items in <code>StorytellerItemView</code>, and uses list wrappers in <code>StoriesListView</code>.</p>"},{"location":"StorytellerListViews/#scrollable-vs-non-scrollable-grids","title":"Scrollable vs non-scrollable Grids","text":"<p>The choice between scrollable and non-scrollable grids applies to both frameworks. In UIKit, non-scrollable grids can be used inside a vertical feed where you might also have rows or other UI elements in the same view. Constrain just the width of the grid and leave its height to be calculated automatically by Auto Layout. Auto-resize can happen whenever the delegate callback <code>onDataLoadComplete</code> is triggered (see more list events).</p> <p>Note - if you use a non-scrollable grid with many items (hundreds) your app might hang when it loads due to the fact that it is not optimized to handle a huge amount of items. For that you would either set a <code>displayLimit</code> or use a scrollable Grid. When no limit is provided, non-scrollable grids default to rendering at most 30 items.</p> <p>Scrollable grids on the other hand support recycling and can be used with a large number of items. It has scrolling and even pull-to-refresh builtin. It is recommended to build a \"More\" screen for example, where the grid is the main view on the screen. You need to constrain both its width and height when integrating it in a view hierarchy.</p>"},{"location":"StorytellerListViews/#interface-builder-support","title":"Interface Builder support","text":"<p>This section applies only to UIKit. UIKit List views support being created in XIBs and storyboards. Type one of the four custom final classes in the Class field in the Identity Inspector of the desired view.</p> <p>Remember grids default to non-scrolling. We added an IBInspectable property so that you can enable scrolling, but it seems Xcode doesn't detect it when using a custom view class from a 3rd party framework. The workaround is to select the grid, open the Identity Inspector, and under User Defined Runtime Attributes add a key path with the value <code>isScrollable</code> of type Boolean and ensure the checkbox next to it is selected.</p> <p>For examples on how to construct the lists in various ways, refer to the Showcase implementations of rows/grids in <code>StoriesListView</code> and list callbacks in <code>StorytellerItemView.listAction</code>.</p>"},{"location":"StorytellerListViews/#further-configuration","title":"Further configuration","text":"<p>The configuration values in this section are shared by UIKit and SwiftUI. UIKit views receive a configuration through <code>configure(with:)</code>; SwiftUI wrappers receive a model initialized with the same configuration type.</p> <p>UIKit views expose a weak <code>delegate</code> property for list events. Keep that delegate strongly referenced in your app. SwiftUI wrappers instead receive a <code>StorytellerListActionCallback</code> through their <code>action</code> parameter.</p> <p>For stories you pass in a <code>StorytellerStoriesListConfiguration</code>, containing this specific Stories parameter:</p> <ul> <li><code>categories</code> - a list of strings to specify the content from what categories the list will be displaying. Note, this is settable only on Story views.</li> </ul> <p>For clips you pass in a <code>StorytellerClipsListConfiguration</code>, containing this specific Clips parameter:</p> <ul> <li><code>collectionId</code> - can be set only on Clip views, and it represents a string identifying the clip collection to be displayed.</li> </ul> <p><code>StorytellerClipsListConfiguration</code> configures Clip tiles in a row or grid. It is different from <code>StorytellerClipCollectionConfiguration</code>, which configures an embedded or programmatically presented Clip Player, including its destination and Ad placements. See Embedded Clips for the Player configuration.</p> <p>The next parameters are common for both the <code>configuration</code> objects:</p> <ul> <li><code>cellType</code> - choose between round and rectangular cells. The latter have an aspect ratio of 2/3.</li> <li><code>theme</code> - this property defines the overall appearance of the list, as well as the player that is presented when tapping on an item.</li> <li><code>uiStyle</code> - override this to make the list display in dark or light mode. The default is auto, which is in sync with the native iOS user interface style.</li> <li><code>displayLimit</code> - limit the number of items shown in the list. <code>nil</code>, <code>0</code> or negative value means no limit for rows and scrollable grids, non-scrollable grids default to 30 items when no limit is provided.</li> <li><code>visibleTiles</code> - limits the number of visible items in the row list. When set to a non-nil value, the row adjusts its height dynamically. Therefore, avoid setting a fixed row height when configuring this property. This configuration will be automatically adjusted based on the iOS Dynamic Type settings - when users change their text size, the number of visible tiles may be reduced/increased to maintain readability and proper spacing. The configured value represents the target number of tiles at the default system text size. The default value is <code>nil</code>.</li> <li><code>context</code> - optional context data that will be included in analytics callbacks for attribution. This allows you to track which sources drive engagement with your content. When configured, <code>context</code> will be included in all analytics events for interactions with the list content. See Analytics for more details.</li> </ul> <p>After configuring a UIKit view, call <code>reloadData()</code> to perform its initial fetch. Call it again after changing the configuration or whenever you need to refresh its content. SwiftUI wrappers perform their initial load from the supplied model; call <code>reloadData()</code> on the model when you need a later refresh without changing its configuration.</p>"},{"location":"StorytellerListViews/#configure-example","title":"Configure Example","text":"UIKitSwiftUI <p>Create a UIKit view, configure it, then call <code>reloadData()</code> to perform the initial fetch. Call it again whenever you need to refresh the content. Assign a strongly retained <code>StorytellerListViewDelegate</code> before loading when you need lifecycle or interaction callbacks.</p> <pre><code>import StorytellerSDK\n\nlet storytellerStoriesRow = StorytellerStoriesRowView()\nlet storytellerClipsRow = StorytellerClipsRowView()\n\n// Stories configuration with context\nstorytellerStoriesRow.configure(with: StorytellerStoriesListConfiguration(\n    categories: [\"sports\", \"entertainment\"],\n    context: [\n        \"source\": \"home-screen-stories\",\n        \"campaign\": \"summer-league\"\n    ]\n))\nstorytellerStoriesRow.reloadData()\n\n// Clips configuration with context\nstorytellerClipsRow.configure(with: StorytellerClipsListConfiguration(\n    collectionId: \"trending-clips\",\n    context: [\n        \"source\": \"home-screen-clips\",\n        \"campaign\": \"summer-league\"\n    ]\n))\nstorytellerClipsRow.reloadData()\n</code></pre> <p>Construct models from the shared configuration types, retain them as view state, and supply a <code>StorytellerListActionCallback</code> to each wrapper. For a real-world setup, see the Showcase wrappers in <code>StoriesListView</code>.</p> <pre><code>import StorytellerSDK\nimport SwiftUI\n\n@available(iOS 15.0, *)\nstruct StorytellerListsView: View {\n    @State private var storiesModel = StorytellerStoriesListModel(\n        configuration: StorytellerStoriesListConfiguration(\n            categories: [\"sports\"],\n            context: [\"location\": \"stories-grid\"]\n        )\n    )\n    @State private var clipsModel = StorytellerClipsListModel(\n        configuration: StorytellerClipsListConfiguration(\n            collectionId: \"top-plays\",\n            context: [\"location\": \"clips-grid\"]\n        )\n    )\n\n    private let handleListAction: StorytellerListActionCallback = { action in\n        switch action {\n        case .onDataLoadStarted:\n            print(\"Storyteller list started loading\")\n        case .onDataLoadComplete(let success, let error, let dataCount):\n            print(\"Loaded \\(dataCount) items; success: \\(success); error: \\(error?.localizedDescription ?? \"none\")\")\n        case .onTileTapped(let type):\n            print(\"Tapped Storyteller tile: \\(type)\")\n        case .onPlayerDismissed:\n            print(\"Storyteller Player dismissed\")\n        @unknown default:\n            break\n        }\n    }\n\n    var body: some View {\n        VStack {\n            StorytellerStoriesGrid(\n                isScrollable: true,\n                model: storiesModel,\n                action: handleListAction\n            )\n            .frame(height: 300)\n\n            StorytellerClipsGrid(\n                isScrollable: true,\n                model: clipsModel,\n                action: handleListAction\n            )\n            .frame(height: 300)\n        }\n        .refreshable {\n            storiesModel.reloadData()\n            clipsModel.reloadData()\n        }\n    }\n}\n</code></pre> <p>Use the same models with <code>StorytellerStoriesRow</code> or <code>StorytellerClipsRow</code> when you need horizontal rows. SwiftUI reloads a component when its model changes; call the model's <code>reloadData()</code> method to reload without changing its configuration.</p>"},{"location":"StorytellerListViews/#continue-your-integration","title":"Continue Your Integration","text":"<ul> <li>Use Storyteller Delegates to choose the correct global or component callback route.</li> <li>Use Embedded Clips when you need a Clip Player embedded in your hierarchy rather than Clip tiles in a row or grid.</li> <li>Use Storyteller Home when Storyteller should provide the complete CMS-configured feed.</li> <li>See Custom Themes and Analytics context for shared list configuration.</li> </ul>"},{"location":"StorytellerModule/","title":"StorytellerModule","text":"<p>The StorytellerModule module is a protocol you can adopt to handle fetching ads and recording user activity events from Storyteller.</p>"},{"location":"StorytellerModule/#properties","title":"Properties","text":""},{"location":"StorytellerModule/#adsource","title":"adSource","text":"<p><code>adSource: StorytellerAdSource?</code> identifies the ad source for ads provided by your module.</p> <ul> <li>Use <code>.custom(\"myNetwork\")</code> for a custom integrating-app source.</li> <li>Use <code>.gam</code> for Google Ad Manager and <code>.admob</code> for Google AdMob.</li> <li>The SDK-provided VAST module uses <code>.custom(\"vast\")</code>.</li> <li><code>.storyteller</code> is reserved for Storyteller First Party ads.</li> <li>Return <code>nil</code> when no source should be attached.</li> </ul> <p>This value is attached to ad analytics payloads when available.</p> <p>If not implemented, the default value is <code>nil</code>.</p> <p>The SDK-provided <code>StorytellerGAMModule</code>, <code>StorytellerAdMobModule</code>, and <code>StorytellerVASTModule</code> set this value automatically.</p> <p>See how the Showcase app registers modules in <code>AppDelegate.setupStoryteller</code>.</p>"},{"location":"StorytellerModule/#methods","title":"Methods","text":""},{"location":"StorytellerModule/#analytics","title":"Analytics","text":"<p>The callback <code>onUserActivityOccurred</code> provides analytics events and corresponding data triggered internally by the SDK. This information can be used in your app.</p> <p>The following parameters are passed to the callback method:</p> <ul> <li><code>type</code> - type of event that occurred, as a <code>StorytellerUserActivity.EventType</code> enum</li> <li><code>data</code> - an object containing data about the event which occurred</li> </ul> <p>Example:</p> <pre><code>func onUserActivityOccurred(type: StorytellerUserActivity.EventType, data: StorytellerUserActivityData) {\n    if type == .OpenedStory {\n        // Retrieve the story id value\n        let openedStoryId = data.storyId\n        // Retrieve the story title value\n        let openedStoryTitle = data.storyTitle\n\n        // Report retrieved values from your app\n    }\n}\n</code></pre> <p>For a detailed discussion of all the relevant events and properties please see the dedicated Analytics page.</p>"},{"location":"StorytellerModule/#ads","title":"Ads","text":"<p>By implementing <code>getAd</code> and <code>getBottomBannerAd</code>, you can provide custom ad data for the SDK to render. This is only applicable when the ad configuration is set to <code>Integrating App</code> in the CMS. Ad data can be obtained asynchronously using async/await, and should be returned directly or throw an error if no ad is available.</p> <p>When building ad CTAs, <code>StorytellerAdAction</code> supports <code>StorytellerActionType.web</code>, <code>StorytellerActionType.inApp</code>, <code>StorytellerActionType.externalApp</code>, and <code>StorytellerActionType.store</code>.</p>"},{"location":"StorytellerModule/#getad","title":"getAd","text":"<p>The <code>getAd</code> method is called when the SDK needs a fullscreen ad:</p> <pre><code>final class CustomAdsModule: StorytellerModule {\n    enum AdLoadingError: Error {\n        case unavailable\n    }\n\n    let loadAd: () async -&gt; StorytellerAd?\n\n    init(loadAd: @escaping () async -&gt; StorytellerAd?) {\n        self.loadAd = loadAd\n    }\n\n    func getAd(for adRequestInfo: StorytellerAdRequestInfo) async throws -&gt; StorytellerAd {\n        guard let ad = await loadAd() else {\n            throw AdLoadingError.unavailable\n        }\n        return ad\n    }\n}\n</code></pre>"},{"location":"StorytellerModule/#bottom-banner-ads","title":"Bottom Banner Ads","text":"<p>The <code>getBottomBannerAd</code> method is called when the SDK needs a bottom banner ad (displayed at the bottom of clips). The <code>maxHeight</code> parameter indicates the maximum allowed banner height for the current layout:</p> <pre><code>final class CustomBannerAdsModule: StorytellerModule {\n    enum AdLoadingError: Error {\n        case unavailable\n    }\n\n    let loadBannerAd: (CGFloat) async -&gt; StorytellerAd?\n\n    init(loadBannerAd: @escaping (CGFloat) async -&gt; StorytellerAd?) {\n        self.loadBannerAd = loadBannerAd\n    }\n\n    func getBottomBannerAd(for adRequestInfo: StorytellerAdRequestInfo, maxHeight: CGFloat) async throws -&gt; StorytellerAd {\n        guard let ad = await loadBannerAd(maxHeight) else {\n            throw AdLoadingError.unavailable\n        }\n        return ad\n    }\n}\n</code></pre>"},{"location":"StorytellerModule/#request-aware-ad-modules","title":"Request-aware ad modules","text":"<p>Host-provided GAM or AdMob modules that know the exact ad unit only when starting a provider request can adopt <code>StorytellerAdRequestTrackingModule</code>. Set <code>adSource</code> to <code>.gam</code> or <code>.admob</code> so the SDK emits paid operational events. The protocol extends <code>StorytellerModule</code> with request-aware full-screen and bottom-banner methods:</p> <ul> <li><code>getAdWithRequestTracking(for:slot:onAdRequested:)</code></li> <li><code>getBottomBannerAdWithRequestTracking(for:maxHeight:onAdRequested:)</code></li> </ul> <p>Await <code>onAdRequested</code> immediately before each concrete provider load. Call it once for each native or banner attempt, including fallback attempts, invoke it serially, and do not retain it after the method returns. The SDK uses each reported value for Google <code>AdRequested</code> events and correlates the final attempt with load, failure, and paid events.</p> <p>Set the same value on the returned <code>StorytellerAd.adUnitId</code> so rendered Ad lifecycle events retain it. The optional <code>slot</code> identifies the active full-screen request; modules that support cancelling their underlying load should honor its cancellation state. SDK-provided GAM and AdMob modules adopt this capability automatically.</p> <p>For a detailed discussion of all the relevant considerations, please see the dedicated Ads page.</p>"},{"location":"StorytellerModule/#technical-consideration","title":"Technical Consideration","text":"<p>Because the <code>StorytellerDelegate</code> also conforms to <code>StorytellerModule</code>, the way our SDK works is as following:</p> <ul> <li>whenever ads are requested, each module has a chance to fetch modules, in the order they appear in the <code>modules</code> array. If a module throws an error, the next one will be queried. Lastly the delegate is requested to provide an ad. If all fail to return an ad, no ad is shown.</li> <li>whenever <code>onUserActivityOccurred</code> is called, all modules will process the event, in the same order, and lastly the delegate will do the same.</li> </ul>"},{"location":"Themes/","title":"Themes","text":"<p>The appearance of the SDK can be customized by setting the <code>theme</code> property. This requires a <code>StorytellerTheme</code> configurable object.</p> <pre><code>let myTheme = StorytellerTheme()\nStoryteller.shared.theme = myTheme\n</code></pre> <p>This object is used as the global theme for every StorytellerListView included in your application. We, therefore, recommend setting it as early as possible in the application's lifecycle (for example, in the <code>didFinishLaunchingWithOptions</code> callback in the <code>AppDelegate</code>).</p> <p>It is also possible to apply a specific theme to a specific StorytellerListView instance by passing it in the configuration. For example:</p> <pre><code>let myRowTheme = StorytellerTheme()\nlet storytellerStoriesRow = StorytellerStoriesRowView()\nstorytellerStoriesRow.configure(with: StorytellerStoriesListConfiguration(\n    categories: [\"category-id\"],\n    theme: myRowTheme\n))\n</code></pre> <p>For more information on setting a theme on an individual list, see StorytellerListView.</p> <p>The Showcase app keeps all theme customization in a single place; see <code>StorytellerThemeManager.globalTheme</code> for a full example (colors, fonts, list styling, and per-item overrides).</p> <p>If a supported change does not appear, follow Appearance or Configuration Does Not Change to check global and per-component precedence, the active light/dark branch, and reload behavior.</p>"},{"location":"Themes/#configuring-a-storytellertheme","title":"Configuring a StorytellerTheme","text":"<p>A <code>StorytellerTheme</code> consists of the following properties:</p> <ul> <li><code>light</code> - sets the <code>Theme</code> to apply for light mode.</li> <li><code>dark</code> - sets the <code>Theme</code> to apply for dark mode.</li> </ul> <p>Which property is used depends on the <code>uiStyle</code> property of the StorytellerListView.</p>"},{"location":"Themes/#creating-themes","title":"Creating Themes","text":"<p>The <code>Theme</code> object contains all of the properties which can be customized in the SDK.</p> <p>Some properties take their default value from others. For example, setting the <code>primaryColor</code> to <code>#FF0000</code> will also result in the New Indicator for Rectangular Tiles being colored red. Such properties are indicated in the table below.</p> <p>Note that theme properties may be used for other situations in future.</p>"},{"location":"Themes/#colors","title":"Colors","text":"<p>The <code>colors</code> property on theme is used to establish a set of base colors for the SDK to use.</p> Property Default Value Data Type Description <code>primary</code> <code>#1C62EB</code> <code>UIColor</code> The default accent color used throughout the UI. In general, this should be the primary brand color. <code>success</code> <code>#3BB327</code> <code>UIColor</code> Used to indicate correct answers in Quizzes. <code>alert</code> <code>#E21219</code> <code>UIColor</code> Used to indicate incorrect answers in Quizzes. <code>white.primary</code> <code>#FFFFFF</code> <code>UIColor</code> Used for white text <code>white.secondary</code> <code>white.primary</code> at 85% opacity <code>UIColor</code> Used for light text <code>white.tertiary</code> <code>white.primary</code> at 70% opacity <code>UIColor</code> Used for gray text <code>black.primary</code> <code>#000000</code> <code>UIColor</code> Used for black text <code>black.secondary</code> <code>black.primary</code> at 85% opacity <code>UIColor</code> Used for light black text <code>black.tertiary</code> <code>black.primary</code> at 70% opacity <code>UIColor</code> Used for gray text"},{"location":"Themes/#font","title":"Font","text":"<p>Use the <code>customFont</code> property on the light and dark themes to set a custom font throughout the UI.</p> <p><code>StorytellerFontProvider</code> is an open class. Subclass it and override <code>font(weight:size:)</code> to provide the appropriate <code>UIFont</code> for each requested weight:</p> <pre><code>import StorytellerSDK\nimport UIKit\n\nfinal class CustomFontProvider: StorytellerFontProvider, @unchecked Sendable {\n    override func font(weight: StorytellerFontWeight, size: CGFloat) -&gt; UIFont? {\n        switch weight {\n        case .light, .regular, .medium:\n            return UIFont(name: \"MyCustomFont\", size: size)\n        default:\n            return UIFont(name: \"MyCustomFontBold\", size: size)\n        }\n    }\n}\n\nvar theme = StorytellerTheme()\nlet customFont = CustomFontProvider()\ntheme.light.customFont = customFont\ntheme.dark.customFont = customFont\nStoryteller.shared.theme = theme\n</code></pre> <p><code>StorytellerFontWeight</code> includes <code>.light</code> for custom providers that expose a light face. Existing integrations with an exhaustive switch over this public enum must add <code>.light</code> or an <code>@unknown default</code> branch when recompiling with SDK 11.5.1.</p>"},{"location":"Themes/#primitives","title":"Primitives","text":"<p>The <code>primitives</code> object contains base values which are used throughout the UI.</p> Property Default Value Data Type Description <code>cornerRadius</code> <code>8</code> <code>CGFloat</code> The corner radius used for rectangular tiles, buttons and poll/quiz answers"},{"location":"Themes/#lists","title":"Lists","text":"<p>The <code>lists</code> customizes properties of the various list types available from the SDK.</p> Property Default Value Data Type Description <code>backgroundColor</code> <code>colors.white.primary</code> <code>UIColor</code> Required for outline on Live chip and fade to the side of the row on the Web SDK <code>enablePlayerOpen</code> <code>true</code> <code>Bool</code> Controls whether the SDK opens the player when a tile is tapped. When set to <code>false</code>, the SDK will not open the player and the app must handle tile taps via <code>StorytellerListViewDelegate.onTileTapped(type: StorytellerTileType)</code> (includes <code>categories: [StorytellerCategoryDetail]</code> for both clips and stories). This setting is not applied for lists on SDK\u2011owned screens (Storyteller Home, Followable Categories, and Search) where the SDK always opens the player. <code>animateTilesOnReorder</code> <code>true</code> <code>Bool</code> When the <code>reloadData()</code> method is called to update lists, a reorder animation is added to visualise the updating process. <code>row.tileSpacing</code> <code>8</code> <code>CGFloat</code> The space between each Tile in a row <code>row.startInset</code> <code>12</code> <code>CGFloat</code> The space before the first Tile in a row <code>row.endInset</code> <code>12</code> <code>CGFloat</code> The space after the last Tile in a row <code>grid.tileSpacing</code> <code>8</code> <code>CGFloat</code> The space between each Tile in a grid, both vertically and horizontally <code>grid.columns</code> <code>2</code> <code>CGFloat</code> The number of columns in a grid. Not applicable to Search and Category screens. <code>grid.topInset</code> <code>12</code> <code>CGFloat</code> The space before the first row in a grid <code>grid.bottomInset</code> <code>12</code> <code>CGFloat</code> The space after the last row in a grid <code>title.font</code> <code>nil</code> <code>StorytellerFontProvider</code> Defines the font of the Title in Section <code>title.textSize</code> <code>22</code> <code>CGFloat</code> Size of the Title in Section <code>title.lineHeight</code> <code>28</code> <code>CGFloat</code> The line height of the Title in Section <code>title.textCase</code> <code>default</code> <code>StorytellerTextCasing</code> Sets the text case for the Title in Section. Possible values are <code>upper</code>, <code>lower</code> and <code>default</code> <code>title.textColor</code> <code>nil</code> <code>UIColor</code> Color of Title in Section"},{"location":"Themes/#gradient","title":"Gradient","text":"<p>The <code>Gradient</code> struct allows for the creation of a color gradient, with options to customize both the colors and the positions at which the gradient starts and ends.</p> Property Default Value Data Type Description <code>startColor</code> Required <code>UIColor</code> The color where the gradient begins. <code>endColor</code> Required <code>UIColor</code> The color where the gradient ends. <code>startPosition</code> Required <code>Theme.Gradient.GradientPosition</code> The position indicating where the gradient starts. <code>endPosition</code> Required <code>Theme.Gradient.GradientPosition</code> The position indicating where the gradient ends."},{"location":"Themes/#enum-gradientposition","title":"Enum: GradientPosition","text":"<p>Defines positions for starting and ending points of the gradient.</p> Value Description <code>bottomLeft</code> Bottom left corner of the gradient area. <code>bottomCenter</code> Bottom center edge of the gradient area. <code>bottomRight</code> Bottom right corner of the gradient area. <code>centerLeft</code> Center left edge of the gradient area. <code>centerCenter</code> Center of the gradient area. <code>centerRight</code> Center right edge of the gradient area. <code>topLeft</code> Top left corner of the gradient area. <code>topCenter</code> Top center edge of the gradient area. <code>topRight</code> Top right corner of the gradient area. <p><code>GradientPosition</code> is a nested enum, not an <code>Int</code>-backed value. Construct a gradient by supplying all four required values:</p> <pre><code>import StorytellerSDK\nimport UIKit\n\nlet brandGradient = Theme.Gradient(\n    startColor: UIColor.systemBlue,\n    endColor: UIColor.systemPurple,\n    startPosition: .topCenter,\n    endPosition: .bottomCenter\n)\n</code></pre>"},{"location":"Themes/#tiles","title":"Tiles","text":"<p>The <code>tiles</code> property can be used to customize the appearance of the Tiles.</p> Property Default Value Data Type Description <code>chip.textSize</code> <code>11</code> <code>CGFloat</code> Text size for the New Indicator and Live Indicator. <code>chip.show</code> <code>true</code> <code>Bool</code> Used to show/hide the new/live chip <code>title.textSize</code> <code>11</code> <code>CGFloat</code> Size of the Title on a Tile <code>title.lineHeight</code> <code>13</code> <code>CGFloat</code> The line height of the Title on a Tile <code>title.alignment</code> <code>center</code> <code>StorytellerAlignment</code> The alignment of the Title on a Tile. Possible values are <code>left</code>, <code>center</code> and <code>right</code> <code>circularTile.title.unreadTextColor</code> inherits <code>colors.black.primary</code> for <code>light</code>, <code>colors.white.primary</code> for <code>dark</code> <code>UIColor</code> The text color of the Title for a circular tile when the story or the clip is unread <code>circularTile.title.readTextColor</code> inherits <code>colors.black.tertiary</code> for <code>light</code>, <code>colors.white.tertiary</code> for <code>dark</code> <code>UIColor</code> The text color of the Tile for a circular tile when the story or the clip is read <code>circularTile.unreadIndicatorColor</code> inherits <code>colors.primary</code> <code>UIColor</code> The color of the ring around a circular tile when the story or the clip is unread <code>circularTile.readIndicatorColor</code> <code>#C5C5C5</code> <code>UIColor</code> The color of the ring around a circular tile when the story or the clip is read <code>circularTile.unreadIndicatorGradient</code> <code>nil</code> <code>Gradient?</code> The gradient of the ring around a circular tile when the story or the clip is unread. If set, overrides <code>circularTile.unreadIndicatorColor</code> <code>circularTile.unreadIndicatorBorderColor</code> <code>nil</code> <code>UIColor?</code> The border color of the ring around a circular tile when the story or the clip is unread <code>circularTile.readIndicatorBorderColor</code> <code>nil</code> <code>UIColor?</code> The border color of the ring around a circular tile when the story or the clip is read <code>circularTile.unreadBorderWidth</code> <code>2</code> <code>CGFloat</code> The width of Circular Tile ring border in unread state <code>circularTile.readBorderWidth</code> <code>2</code> <code>CGFloat</code> The width of Circular Tile ring border in read state <code>circularTile.liveChip.readImage</code> <code>null</code> <code>UIImage?</code> Image to be used in place of default read Live Indicator. <code>circularTile.liveChip.unreadImage</code> <code>null</code> <code>UIImage?</code> Image to be used in place of default unread Live Indicator <code>circularTile.liveChip.unreadBackgroundColor</code> <code>colors.alert</code> <code>UIColor</code> Background color of the Live Indicator when the story contains unread pages or the clip has not been viewed. <code>circularTile.liveChip.readBackgroundColor</code> <code>colors.black.tertiary</code> <code>UIColor</code> Background color of the Live Indicator when all story pages have been read or the clip has been viewed. <code>circularTile.liveChip.unreadBackgroundGradient</code> <code>nil</code> <code>Gradient?</code> The gradient of the ring around a live tile and background of the Live Indicator. If set, overrides <code>circularTile.liveChip.unreadBackgroundColor</code> <code>circularTile.liveChip.unreadTextColor</code> <code>colors.white.primary</code> <code>UIColor</code> Text color of the Live Indicator when the story contains unread pages or the clip has not been viewed. <code>circularTile.liveChip.readTextColor</code> <code>colors.white.primary</code> <code>UIColor</code> Text color of the Live Indicator when all story pages have been read or the clip has been viewed. <code>circularTile.liveChip.unreadBorderColor</code> <code>null</code> <code>UIColor?</code> Border color of the Live Indicator when the story contains unread pages or the clip has not been viewed. <code>circularTile.liveChip.readBorderColor</code> <code>null</code> <code>UIColor?</code> Boder color of the Live Indicator when all story pages have been read or the clip has been viewed. <code>rectangularTile.padding</code> <code>8</code> <code>CGFloat</code> The internal padding for a rectangular story or the clip tile <code>rectangularTile.title.textColor</code> inherits <code>colors.white.primary</code> <code>UIColor</code> The text color of the Title for a rectangular tile <code>rectangularTile.chip.alignment</code> <code>right</code> <code>StorytellerAlignment</code> The alignment of the New Indicator and Live Indicator in Rectangular Tiles. Possible values are <code>left</code>, <code>center</code> or <code>right</code> <code>rectangularTile.unreadIndicator.image</code> <code>null</code> <code>UIImage?</code> An image which can be used in place of the default unread indicator for a rectangular tile. If set, overrides <code>rectangularTile.unreadIndicator.gradient</code> <code>rectangularTile.unreadIndicator.gradient</code> <code>nil</code> <code>Gradient?</code> The background gradient of the unread indicator for a rectangular tile. If set, overrides <code>rectangularTile.unreadIndicator.backgroundColor</code> <code>rectangularTile.unreadIndicator.backgroundColor</code> inherits <code>colors.primary</code> <code>UIColor</code> The background color of the unread indicator for a rectangular tile <code>rectangularTile.unreadIndicator.textColor</code> inherits <code>colors.white.primary</code> <code>UIColor</code> The text color of the unread indicator for a rectangular tile <code>rectangularTile.unreadIndicator.borderColor</code> <code>null</code> <code>UIColor?</code> Border color of the unread indicator for a rectangular tile <code>rectangularTile.liveChip.readImage</code> <code>null</code> <code>UIImage?</code> Image to be used in place of default read Live Indicator. <code>rectangularTile.liveChip.unreadImage</code> <code>null</code> <code>UIImage?</code> Image to be used in place of default unread Live Indicator. If set, overrides <code>rectangularTile.liveChip.unreadBackgroundGradient</code> <code>rectangularTile.liveChip.unreadBackgroundGradient</code> <code>nil</code> <code>Gradient?</code> Gradient background to be used for the Live Indicator. If set, overrides <code>rectangularTile.liveChip.unreadBackgroundColor</code> <code>rectangularTile.liveChip.unreadBackgroundColor</code> <code>colors.alert</code> <code>UIColor</code> Background color of the Live Indicator when the story contains unread pages or the clip has not been viewed. <code>rectangularTile.liveChip.readBackgroundColor</code> <code>colors.black.tertiary</code> <code>UIColor</code> Background color of the Live Indicator when all pages have been read or the clip has been viewed <code>rectangularTile.liveChip.unreadTextColor</code> <code>colors.white.primary</code> <code>UIColor</code> Text color of the Live Indicator when the story contains unread pages or the clip has not been viewed. <code>rectangularTile.liveChip.readTextColor</code> <code>colors.white.primary</code> <code>UIColor</code> Text color of the Live Indicator when all story pages have been read or the clip has been viewed. <code>rectangularTile.liveChip.unreadBorderColor</code> <code>null</code> <code>UIColor?</code> Border color of the Live Indicator when the story contains unread pages or the clip has not been viewed. <code>rectangularTile.liveChip.readBorderColor</code> <code>null</code> <code>UIColor?</code> Border color of the Live Indicator when all story pages have been read or the clip has been viewed. <p></p> <p></p>"},{"location":"Themes/#player","title":"Player","text":"<p>The <code>player</code> property is used to customize properties relating to the Story and Clips Player.</p> Property Default Value Data Type Description <code>showStoryIcon</code> <code>false</code> <code>Bool</code> Shows the round story icon before the Story Title in the Player <code>showTimestamp</code> <code>true</code> <code>Bool</code> Shows the timestamp after the Story Title in the Player, indicating how long ago a story was published <code>showShareButton</code> <code>true</code> <code>Bool</code> Shows the share button in the Player. Setting this to <code>false</code> entirely disables sharing in Storyteller <code>showLikeButton</code> <code>true</code> <code>Bool</code> Shows the like button in the Clips Player. Setting this to <code>false</code> entirely disables liking in Storyteller <code>enableFollowableCategorySwipeFromRightEdge</code> <code>true</code> <code>Bool</code> Enables opening the Followable Category screen with a right-edge swipe in the Clips Player. Set to <code>false</code> to disable this gesture <code>showShareCount</code> <code>true</code> <code>Bool</code> Shows the share count label in the Clips Player. Does not affect share button visibility <code>showLikeCount</code> <code>true</code> <code>Bool</code> Shows the like count label in the Clips Player. Does not affect like button visibility <code>clips.showButtonBackgrounds</code> <code>true</code> <code>Bool</code> Controls whether the Clips Player renders the default button backgrounds for like, share, mute, and captions buttons <code>clips.actionIconSize</code> <code>32</code> <code>CGFloat</code> Size in points for Clips Player action icons. Values outside the supported <code>24...38</code> range are clamped during theme resolution <code>clips.feedSwitcher.selected.fontWeight</code> selection-style default <code>StorytellerFontWeight?</code> Optional selected For You / Following tab weight. When set, it overrides the weight implied by <code>theme.behavior.following.feedSwitcher.selectionStyle</code> <code>clips.feedSwitcher.selected.textSize</code> <code>16</code> <code>CGFloat?</code> Optional selected For You / Following tab text size <code>clips.feedSwitcher.selected.lineHeight</code> <code>nil</code> <code>CGFloat?</code> Optional selected For You / Following tab line height <code>clips.feedSwitcher.unselected.fontWeight</code> selection-style default <code>StorytellerFontWeight?</code> Optional unselected For You / Following tab weight. When set, it overrides the weight implied by <code>theme.behavior.following.feedSwitcher.selectionStyle</code> <code>clips.feedSwitcher.unselected.textSize</code> <code>16</code> <code>CGFloat?</code> Optional unselected For You / Following tab text size <code>clips.feedSwitcher.unselected.lineHeight</code> <code>nil</code> <code>CGFloat?</code> Optional unselected For You / Following tab line height <code>clips.eyebrow.font</code> inherits <code>theme.customFont</code> <code>StorytellerFontProvider?</code> Font family used for the Clips Player eyebrow row. The SDK requests the <code>.regular</code> weight at render time <code>clips.eyebrow.textSize</code> <code>15</code> <code>CGFloat</code> Text size used for the Clips Player eyebrow row <code>clips.eyebrow.lineHeight</code> <code>nil</code> <code>CGFloat?</code> Optional line height for the Clips Player eyebrow row. <code>nil</code> preserves the current UIKit spacing for the resolved font <code>clips.eyebrow.textColor</code> inherits <code>theme.colors.white.secondary</code> <code>UIColor?</code> Text color used for the Clips Player eyebrow row <code>clips.title.font</code> inherits <code>theme.customFont</code> <code>StorytellerFontProvider?</code> Font family used for the Clips Player main title. The SDK requests <code>clips.title.fontWeight</code>, defaulting to <code>.bold</code> <code>clips.title.fontWeight</code> <code>.bold</code> <code>StorytellerFontWeight?</code> Optional weight requested from the Clips Player title font provider <code>clips.title.textSize</code> <code>16</code> <code>CGFloat</code> Text size used for the Clips Player main title <code>clips.title.lineHeight</code> <code>nil</code> <code>CGFloat?</code> Optional line height for the Clips Player main title. <code>nil</code> preserves the system default line spacing for the resolved font <code>clips.title.textColor</code> inherits <code>theme.colors.white.primary</code> <code>UIColor?</code> Text color used for the Clips Player main title <code>clips.categoryNavigation.fontWeight</code> <code>.bold</code> <code>StorytellerFontWeight?</code> Optional category-navigation label weight requested from <code>theme.customFont</code> <code>clips.categoryNavigation.textSize</code> <code>15</code> <code>CGFloat?</code> Optional category-navigation label text size <code>clips.categoryNavigation.lineHeight</code> <code>nil</code> <code>CGFloat?</code> Optional category-navigation label line height <code>clips.topGradient</code> black at 60% opacity from <code>.topCenter</code> to clear at <code>.bottomCenter</code> <code>Theme.Gradient?</code> Customizes the existing 96-point top readability scrim without changing its bounds or visibility <code>clips.bottomGradient</code> clear at <code>.topCenter</code> to black at 60% opacity at <code>.bottomCenter</code> <code>Theme.Gradient?</code> Customizes the existing title-backed bottom readability scrim without changing its bounds or title-empty visibility rule <code>clips.progressBar.position</code> <code>.bottom</code> <code>Theme.Player.Clips.ProgressBar.Position</code> Sets the Clips progress bar to <code>.bottom</code> or <code>.aboveAction</code>. <code>.aboveAction</code> applies only to Embedded Clips with a visible primary action <code>clips.spacing.backButtonStartInset</code> <code>0</code> <code>CGFloat</code> Left inset in points for the Clips Player back/close button <code>clips.spacing.contentInsetHorizontal</code> <code>16</code> <code>CGFloat</code> Horizontal (start/end) padding in points for the description and action content areas <code>clips.spacing.contentInsetBottom</code> <code>16</code> <code>CGFloat</code> Bottom padding in points for the description and action content areas <code>clips.spacing.actionSpacing</code> <code>12</code> <code>CGFloat</code> Vertical spacing in points between action icons (follow, like, share, mute, caption) <code>clips.spacing.eyebrowToTitleSpacing</code> <code>4</code> <code>CGFloat</code> Spacing in points between the eyebrow text and the clip title <code>clips.spacing.titleToActionSpacing</code> <code>16</code> <code>CGFloat</code> Horizontal gap in points between the title/description text and the action icons column <code>clips.spacing.titleToCategoriesSpacing</code> <code>nil</code> <code>CGFloat?</code> Vertical gap in points between the rendered title/description block and categories in Embedded Clips <code>clips.spacing.categoriesToMoreSpacing</code> <code>nil</code> <code>CGFloat?</code> Vertical gap in points between categories and the existing expansion/collapse affordance in Embedded Clips <code>clips.spacing.metadataToProgressBarSpacing</code> <code>nil</code> <code>CGFloat?</code> Minimum vertical gap in points between the metadata block and the visible progress track in Embedded Clips <code>clips.spacing.progressBarToActionSpacing</code> <code>nil</code> <code>CGFloat?</code> Minimum vertical gap in points between the visible progress track and primary action in Embedded Clips, independent of progress-bar ordering <code>liveChip.image</code> <code>null</code> <code>UIImage?</code> Image used in place of Live Chip before Live Story or Clip Titles. If set, it overrides <code>liveChip.backgroundGradient</code> <code>liveChip.textColor</code> <code>null</code> <code>UIColor?</code> Text color used for badge label for Live Story or Clip <code>liveChip.backgroundGradient</code> <code>null</code> <code>Gradient?</code> Background gradient of the badge for Live Story or Clip. If set, it overrides <code>liveChip.backgroundColor</code> <code>liveChip.backgroundColor</code> <code>theme.colors.alert</code> <code>UIColor?</code> Background color of the badge for Live Story or Clip <code>liveChip.borderColor</code> <code>null</code> <code>UIColor</code> Border color of the badge for Live Story or Clip <code>icons.share</code> <code>null</code> <code>UIImage?</code> An image to be used in place of the default share icon <code>icons.refresh</code> <code>null</code> <code>UIImage?</code> Refresh button image to be used in place of refresh share icon, used in the error state <code>icons.back</code> <code>null</code> <code>UIImage?</code> Back button image to be used in place of the default Clips back icon <code>icons.like.initial</code> <code>null</code> <code>UIImage?</code> An image to be used in place of the default like icon when the clip is not liked <code>icons.like.liked</code> <code>null</code> <code>UIImage?</code> An image to be used in place of the default like icon when the clip is liked <code>icons.like.animation.liked</code> <code>null</code> <code>StorytellerPlayerIcons.LikeIcons.Animation.Resource?</code> Bundled Lottie animation played when the Clips like button changes from unliked to liked <code>icons.like.animation.unliked</code> <code>null</code> <code>StorytellerPlayerIcons.LikeIcons.Animation.Resource?</code> Bundled Lottie animation played when the Clips like button changes from liked to unliked <code>icons.mute.muted</code> <code>null</code> <code>UIImage?</code> Image used for the mute button when audio is muted in Stories and Clips <code>icons.mute.unmuted</code> <code>null</code> <code>UIImage?</code> Image used for the mute button when audio is unmuted in Stories and Clips <code>icons.captions.enabled</code> <code>null</code> <code>UIImage?</code> Image used for the captions button when captions are enabled in Stories and Clips <code>icons.captions.disabled</code> <code>null</code> <code>UIImage?</code> Image used for the captions button when captions are disabled in Stories and Clips <p>If only one mute or captions state is provided, the missing state continues to use the bundled Storyteller icon.</p> <p>Example:</p> <pre><code>var theme = StorytellerTheme()\n\ntheme.light.player.clips.showButtonBackgrounds = false\ntheme.light.player.clips.actionIconSize = 24\ntheme.light.player.clips.eyebrow = .init(\n    font: StorytellerFontProvider(),\n    textSize: 20,\n    lineHeight: 24,\n    textColor: UIColor.white\n)\ntheme.light.player.clips.title = .init(\n    font: StorytellerFontProvider(),\n    textSize: 20,\n    lineHeight: 24,\n    textColor: UIColor.white,\n    fontWeight: .regular\n)\ntheme.light.player.clips.feedSwitcher = .init(\n    selected: .init(fontWeight: .regular, textSize: 15, lineHeight: 20),\n    unselected: .init(fontWeight: .light, textSize: 15, lineHeight: 20)\n)\ntheme.light.player.clips.categoryNavigation = .init(\n    fontWeight: .regular,\n    textSize: 13,\n    lineHeight: 16\n)\ntheme.light.player.clips.topGradient = .init(\n    startColor: UIColor.systemBlue.withAlphaComponent(0.8),\n    endColor: .clear,\n    startPosition: .topLeft,\n    endPosition: .bottomRight\n)\ntheme.light.player.clips.bottomGradient = .init(\n    startColor: UIColor.systemPink.withAlphaComponent(0.8),\n    endColor: .clear,\n    startPosition: .bottomRight,\n    endPosition: .topLeft\n)\ntheme.light.player.clips.progressBar.position = .aboveAction\ntheme.light.player.clips.spacing = .init(\n    backButtonStartInset: 12,\n    contentInsetHorizontal: 16,\n    contentInsetBottom: 20,\n    actionSpacing: 20,\n    eyebrowToTitleSpacing: 6,\n    titleToActionSpacing: 16,\n    titleToCategoriesSpacing: 8,\n    categoriesToMoreSpacing: 4,\n    metadataToProgressBarSpacing: 16,\n    progressBarToActionSpacing: 16\n)\ntheme.light.player.icons = StorytellerPlayerIcons(\n    back: UIImage(named: \"icon-back-custom\"),\n    muteMuted: UIImage(named: \"icon-mute-muted-custom\"),\n    muteUnmuted: UIImage(named: \"icon-mute-unmuted-custom\"),\n    captionsEnabled: UIImage(named: \"icon-captions-enabled-custom\"),\n    captionsDisabled: UIImage(named: \"icon-captions-disabled-custom\")\n)\n</code></pre> <p>The canonical key path for this API is <code>theme.player.clips.eyebrow</code>.</p> <p><code>clips.eyebrow.lineHeight</code> intentionally defaults to <code>nil</code> to preserve the current UIKit eyebrow spacing.</p> <p><code>clips.title.lineHeight</code> intentionally defaults to <code>nil</code>, unlike other title theme APIs that ship a non-<code>nil</code> line height. Leaving it unset preserves the current Clips title spacing.</p>"},{"location":"Themes/#clips-player-typography","title":"Clips player typography","text":"<p>The feed switcher and category-navigation labels inherit <code>theme.customFont</code>; they do not have separate font-family properties. The clip title continues to use <code>clips.title.font</code>, falling back to <code>theme.customFont</code>. Each configured weight is requested from that provider.</p> <p>When feed-switcher weights are absent, underline selection mode keeps <code>.semibold</code> for both tabs, while text-weight selection mode keeps <code>.heavy</code> selected and <code>.medium</code> unselected. Explicit selected or unselected weights replace only those fallback weights; <code>theme.behavior.following.feedSwitcher.selectionStyle</code> still controls underline visibility, animation, and the remaining selection behavior.</p> <p>Configured sizes and line heights scale with Dynamic Type. Category names and the delimiter from <code>theme.behavior.player.clips.categoryNavigation.delimiter</code> share one attributed typography style, while the existing <code>less</code> control keeps its current style.</p> <p>Host values and remote light/dark values resolve independently for every typography field. A feed- or collection-specific remote value overrides the tenant/global remote value, which overrides the host-supplied light or dark theme. Remote font-weight strings are case-insensitive, so values such as <code>REGULAR</code>, <code>LIGHT</code>, and <code>BLACK</code> resolve to the corresponding lowercase-backed public enum cases. Missing, unknown, non-finite, or non-positive remote values continue to the next fallback without changing compatibility output.</p> <p>Sky configuration:</p> <pre><code>var theme = StorytellerTheme()\n\ntheme.light.player.clips.feedSwitcher.selected = .init(\n    fontWeight: .regular,\n    textSize: 15,\n    lineHeight: 20\n)\ntheme.light.player.clips.feedSwitcher.unselected = .init(\n    fontWeight: .light,\n    textSize: 15,\n    lineHeight: 20\n)\ntheme.light.player.clips.title.fontWeight = .regular\ntheme.light.player.clips.title.textSize = 15\ntheme.light.player.clips.title.lineHeight = 20\ntheme.light.player.clips.categoryNavigation = .init(\n    fontWeight: .regular,\n    textSize: 13,\n    lineHeight: 16\n)\n</code></pre> <p>Equivalent remote Settings fragment:</p> <pre><code>{\n  \"theme\": {\n    \"light\": {\n      \"player\": {\n        \"clips\": {\n          \"feedSwitcher\": {\n            \"selected\": { \"fontWeight\": \"REGULAR\", \"textSize\": 15, \"lineHeight\": 20 },\n            \"unselected\": { \"fontWeight\": \"LIGHT\", \"textSize\": 15, \"lineHeight\": 20 }\n          },\n          \"title\": { \"fontWeight\": \"REGULAR\", \"textSize\": 15, \"lineHeight\": 20 },\n          \"categoryNavigation\": { \"fontWeight\": \"REGULAR\", \"textSize\": 13, \"lineHeight\": 16 }\n        }\n      }\n    },\n    \"dark\": {\n      \"player\": {\n        \"clips\": {\n          \"feedSwitcher\": {\n            \"selected\": { \"fontWeight\": \"REGULAR\", \"textSize\": 15, \"lineHeight\": 20 },\n            \"unselected\": { \"fontWeight\": \"LIGHT\", \"textSize\": 15, \"lineHeight\": 20 }\n          },\n          \"title\": { \"fontWeight\": \"REGULAR\", \"textSize\": 15, \"lineHeight\": 20 },\n          \"categoryNavigation\": { \"fontWeight\": \"REGULAR\", \"textSize\": 13, \"lineHeight\": 16 }\n        }\n      }\n    }\n  }\n}\n</code></pre>"},{"location":"Themes/#clips-player-gradients","title":"Clips player gradients","text":"<p><code>theme.player.clips.topGradient</code> and <code>theme.player.clips.bottomGradient</code> use the existing two-colour <code>Theme.Gradient</code> type. All nine <code>GradientPosition</code> values are supported for either endpoint, and the start and end positions must be different.</p> <p>When <code>topGradient</code> is absent at every layer, the Clips Player keeps its existing black-at-60%-opacity to clear gradient from <code>.topCenter</code> to <code>.bottomCenter</code> over the fixed 96-point top scrim. When <code>bottomGradient</code> is absent at every layer, it keeps the existing clear-at-top to black-at-60%-opacity-at-bottom appearance over the title-stack-derived bottom scrim. The bottom scrim remains hidden when the Clip title is missing or empty.</p> <p>Each gradient resolves independently in this order: feed- or collection-specific remote light/dark theme, tenant/global remote light/dark theme, host-supplied light/dark theme, then its iOS default. Missing, <code>null</code>, incomplete, malformed, or unknown remote values continue to the next layer for that gradient only; a malformed top value does not discard a valid bottom value, and vice versa.</p> <p>A clear-to-clear gradient is an explicit transparent scrim and does not fall back to the default. This can be used to remove either scrim visually while preserving its existing container and visibility behavior. Custom colours are rendered as supplied, so the integrating app is responsible for maintaining sufficient contrast between video content and Player controls or text.</p> <p>Remote colours accept <code>#RRGGBB</code> or alpha-first <code>#AARRGGBB</code> values. For example:</p> <pre><code>{\n  \"theme\": {\n    \"light\": {\n      \"player\": {\n        \"clips\": {\n          \"topGradient\": {\n            \"startColor\": \"#CC0057B8\",\n            \"endColor\": \"#00000000\",\n            \"startPosition\": \"topLeft\",\n            \"endPosition\": \"bottomRight\"\n          },\n          \"bottomGradient\": {\n            \"startColor\": \"#CCEF3340\",\n            \"endColor\": \"#00000000\",\n            \"startPosition\": \"bottomRight\",\n            \"endPosition\": \"topLeft\"\n          }\n        }\n      }\n    }\n  }\n}\n</code></pre>"},{"location":"Themes/#embedded-clips-spacing","title":"Embedded Clips spacing","text":"<p>The four optional internal spacing properties apply only to Embedded Clips. Each property resolves independently through the active feed-specific server appearance, tenant/global server appearance, and host-supplied light or dark theme. Missing, non-finite, or negative values are treated as unset and preserve the existing iOS layout for that relationship.</p> <p>Configured spacing is added only when both rendered elements are present. The fixed point values do not scale with Dynamic Type, while the surrounding metadata continues to reflow. The Sky configuration uses <code>8</code>, <code>4</code>, <code>16</code>, and <code>16</code> points for title-to-categories, categories-to-expansion, metadata-to-progress, and progress-to-action respectively.</p> <p>Example:</p> <pre><code>var theme = StorytellerTheme()\n\ntheme.light.player.icons = StorytellerPlayerIcons(\n    back: UIImage(named: \"icon-back-custom\"),\n    likeInitial: UIImage(named: \"custom_like_initial\"),\n    likeLiked: UIImage(named: \"custom_like_liked\"),\n    likeAnimation: .init(\n        liked: .init(name: \"custom_like_liked_animation\"),\n        unliked: .init(name: \"custom_like_unliked_animation\")\n    )\n)\n</code></pre> <p>You can omit <code>likeInitial</code> and <code>likeLiked</code> when you want to keep the SDK's default static heart icons and only override the transition animations.</p> <p>Tenant settings can also provide static Clips Player like icons through the remote theme payload. Use complete URL pairs under the light and/or dark style theme:</p> <pre><code>{\n  \"light\": {\n    \"player\": {\n      \"icons\": {\n        \"like\": {\n          \"initial\": \"https://example.com/like.png\",\n          \"liked\": \"https://example.com/liked.png\"\n        }\n      }\n    }\n  }\n}\n</code></pre> <p>Both <code>initial</code> and <code>liked</code> must be provided for the SDK to replace the default static like icons. Remote settings do not configure <code>icons.like.animation</code>; use the local <code>StorytellerTheme</code> API for bundled Lottie animations.</p> <p></p>"},{"location":"Themes/#clips-progress-bar-position","title":"Clips progress bar position","text":"<p><code>theme.player.clips.progressBar.position</code> controls the progress bar hierarchy for Clips. It supports the following appearance-scoped values:</p> Value Behavior <code>.bottom</code> / <code>bottom</code> Preserves the existing progress bar position. This is the default <code>.aboveAction</code> / <code>aboveAction</code> Places the progress bar above the visible primary action in Embedded Clips <p>When <code>.aboveAction</code> is selected, modal and full-screen Clips remain unchanged. Embedded Clips without a visible primary action also keep the stable bottom position without reserving an empty action gap. The scrub gesture area follows the rendered progress bar in both positions.</p> <p>The active light or dark value resolves in this order: feed-specific remote theme, tenant/global remote theme, host-supplied theme, then <code>.bottom</code>. Unknown or malformed remote values are ignored so normal inheritance can continue.</p> <p>Example remote Settings fragment:</p> <pre><code>{\n  \"theme\": {\n    \"light\": {\n      \"player\": {\n        \"clips\": {\n          \"progressBar\": {\n            \"position\": \"aboveAction\"\n          }\n        }\n      }\n    }\n  }\n}\n</code></pre>"},{"location":"Themes/#modal-clips-content-bottom-anchor","title":"Modal Clips content bottom anchor","text":"<p><code>theme.behavior.player.clips.modalContentBottomAnchor</code> is a remote Settings property for non-embedded modal Clips Players. It accepts the following values:</p> Value Behavior <code>video</code> Keeps the title, metadata, side actions, primary action, and progress controls anchored to the bottom of the 9:16 media frame. This is the default and preserves the existing layout <code>screen</code> Keeps the media top-aligned at 9:16 while anchoring the lower Player UI to the viewport safe-area bottom, using the space below the media on taller screens <p>Missing, <code>null</code>, malformed, or unknown values resolve to <code>video</code>. The setting does not stretch or crop video, does not affect Embedded Clips, and keeps standard Ads on the existing video-anchored layout.</p> <p>Example remote Settings fragment:</p> <pre><code>{\n  \"theme\": {\n    \"behavior\": {\n      \"player\": {\n        \"clips\": {\n          \"modalContentBottomAnchor\": \"screen\"\n        }\n      }\n    }\n  }\n}\n</code></pre>"},{"location":"Themes/#followable-category-profile","title":"Followable Category Profile","text":"<p>The default Followable Category profile screen is configured from remote Settings via the CMS. There is no SDK public <code>Theme</code> API for these profile fields.</p> <p>Set <code>theme.behavior.player.clips.enableProfileScreen</code> to <code>true</code> to use the profile screen. When this value is <code>false</code> or missing, the SDK uses the legacy Followable Category screen. A custom category screen returned by <code>StorytellerDelegate.viewController(for:)</code> always takes priority over either SDK screen.</p> <p>Profile appearance is configured independently under <code>theme.light.profileScreen</code> and <code>theme.dark.profileScreen</code>:</p> Property Default Value Data Type Description <code>contentAvailability.clips</code> <code>true</code> <code>Bool</code> Shows the Latest/Popular Clips feed <code>contentAvailability.stories</code> <code>false</code> <code>Bool</code> Shows the Stories row sourced from the category display title <code>displayTitle.textSize</code> <code>22</code> <code>Int</code> Category title text size <code>displayTitle.lineHeight</code> <code>28</code> <code>Int</code> Category title line height <code>displayTitle.textCase</code> <code>default</code> <code>String</code> <code>default</code>, <code>upper</code>, or <code>lower</code> <code>displayTitle.textColor</code> <code>#FFFFFF</code> <code>String</code> Category title color <code>description.textSize</code> <code>16</code> <code>Int</code> Category description text size <code>description.lineHeight</code> <code>20</code> <code>Int</code> Category description line height <code>description.textCase</code> <code>default</code> <code>String</code> <code>default</code>, <code>upper</code>, or <code>lower</code> <code>description.textColor</code> <code>#D9FFFFFF</code> <code>String</code> Category description color <code>followButton.cornerRadius</code> <code>8</code> <code>Int</code> Follow/Unfollow button corner radius <code>followButton.title.textSize</code> <code>14</code> <code>Int</code> Button title text size <code>followButton.title.lineHeight</code> <code>20</code> <code>Int</code> Button title line height <code>followButton.title.textCase</code> <code>default</code> <code>String</code> <code>default</code>, <code>upper</code>, or <code>lower</code> <code>followButton.followed.textColor</code> <code>#FFFFFF</code> <code>String</code> Unfollow-state title color <code>followButton.followed.backgroundColor</code> <code>#33FFFFFF</code> <code>String</code> Unfollow-state background color <code>followButton.unfollowed.textColor</code> <code>#FFFFFF</code> <code>String</code> Follow-state title color <code>followButton.unfollowed.backgroundColor</code> <code>#1C62EB</code> <code>String</code> Follow-state background color <code>tabs.title.textSize</code> <code>20</code> <code>Int</code> Latest/Popular title text size <code>tabs.title.lineHeight</code> <code>24</code> <code>Int</code> Latest/Popular title line height <code>tabs.title.textCase</code> <code>default</code> <code>String</code> <code>default</code>, <code>upper</code>, or <code>lower</code> <code>tabs.selectedTextColor</code> Light: <code>#FF1A1A1A</code>; Dark: <code>#FFFFFFFF</code> <code>String</code> Selected tab title color <code>tabs.unselectedTextColor</code> Light: <code>#991A1A1A</code>; Dark: <code>#D9FFFFFF</code> <code>String</code> Unselected tab title color <p>Colors accept six-digit RGB (<code>#RRGGBB</code>) or eight-digit alpha-first ARGB (<code>#AARRGGBB</code>). Missing, <code>null</code>, malformed, or out-of-range fields fall back independently, so one invalid field does not discard valid sibling values. If both content availability values resolve to <code>false</code>, the SDK falls back to Clips enabled and Stories disabled.</p> <p>Missing Profile tab colours follow the active appearance. Automatic UI style uses the system Light or Dark appearance, while forced Light or Dark styles use their corresponding fallback values. Explicitly configured selected or unselected colours take precedence independently.</p> <p>Example remote Settings fragment:</p> <pre><code>{\n  \"theme\": {\n    \"behavior\": {\n      \"player\": {\n        \"clips\": {\n          \"enableProfileScreen\": true\n        }\n      }\n    },\n    \"light\": {\n      \"profileScreen\": {\n        \"contentAvailability\": {\n          \"clips\": true,\n          \"stories\": true\n        },\n        \"displayTitle\": {\n          \"textColor\": \"#FF1A1A1A\"\n        },\n        \"tabs\": {\n          \"selectedTextColor\": \"#FF1A1A1A\",\n          \"unselectedTextColor\": \"#991A1A1A\"\n        }\n      }\n    }\n  }\n}\n</code></pre> <p>Text uses the resolved SDK theme font. The profile image comes from the CMS and its border is not themeable. <code>theme.light.categoryScreen.followIcons</code> and <code>theme.dark.categoryScreen.followIcons</code> continue to configure the legacy screen and are separate from <code>profileScreen</code>.</p>"},{"location":"Themes/#cards","title":"Cards","text":"<p>The <code>cards</code> property applies customizations to Storyteller Cards. Cards audio behavior is controlled by tenant settings, while local theme values can replace the icons used by the Cards audio control.</p> Property Default Value Data Type Description <code>audio.mutedIcon</code> <code>nil</code> <code>UIImage?</code> Complete 48x48 visual used for the Cards audio control when the active video Card is muted <code>audio.unmutedIcon</code> <code>nil</code> <code>UIImage?</code> Complete 48x48 visual used for the Cards audio control when the active video Card is unmuted <p>If a Cards audio icon is not provided, the SDK uses a default 48x48 tap target with a bundled 32x32 circular state icon centered inside it. Custom <code>theme.cards.audio</code> images replace that whole visual directly, without an SDK-drawn circle/background; include any desired backer in the image asset. <code>theme.player.icons.mute</code> customizes Stories and Clips Player mute icons; use <code>theme.cards.audio</code> for Cards.</p> <p>Example:</p> <pre><code>var theme = StorytellerTheme()\n\ntheme.light.cards.audio = .init(\n    mutedIcon: UIImage(named: \"icon-card-audio-muted\"),\n    unmutedIcon: UIImage(named: \"icon-card-audio-unmuted\")\n)\n</code></pre>"},{"location":"Themes/#buttons","title":"Buttons","text":"<p>The <code>buttons</code> property applies customizations to buttons which appear throughout the UI.</p> Property Default Value Data Type Description <code>backgroundColor</code> inherits <code>colors.white.primary</code> <code>UIColor</code> The background color of buttons throughout the UI <code>textColor</code> inherits <code>colors.black.primary</code> <code>UIColor</code> The text color of buttons throughout the SDK <code>textCase</code> <code>default</code> <code>StorytellerTextCasing</code> Sets the text case for buttons throughout the UI. Possible values are <code>upper</code>, <code>lower</code> and <code>default</code> <code>cornerRadius</code> inherits <code>primitives.cornerRadius</code> <code>CGFloat</code> The corner radius for all buttons throughout the UI"},{"location":"Themes/#instructions","title":"Instructions","text":"<p>Use the <code>instructions</code> property to customize the appearance of the instructions screen.</p> Property Default Value Data Type Description <code>show</code> <code>true</code> <code>Bool</code> Determines whether the Instructions Screen is shown the first time a user opens the Story Player. Set to <code>false</code> to completely disable the instructions screen. <code>headingColor</code> inherits <code>colors.black.primary</code> for <code>light</code>, <code>colors.white.primary</code> for <code>dark</code> <code>UIColor</code> The color of the heading text on the Instructions Screen <code>headingTextCase</code> <code>default</code> <code>StorytellerTextCasing</code> Determines the text case of the heading on the Instructions Screen. Possible values are <code>upper</code>, <code>lower</code> and <code>default</code> <code>headingFont</code> <code>null</code> <code>StorytellerFontProvider</code> Defines the font of the heading text on the Instructions Screen <code>subHeadingColor</code> inherits <code>colors.black.secondary</code> for <code>light</code>, <code>colors.white.secondary</code> for <code>dark</code> <code>UIColor</code> The color of the subheading text on the Instructions Screen <code>backgroundColor</code> inherits <code>colors.white.primary</code> for <code>light</code>, <code>colors.black.primary</code> for <code>dark</code> <code>UIColor</code> The color of the background of the Instructions Screen <code>icons</code> <code>null</code> <code>StorytellerInstructionIcons</code> A set of custom icons to be used for each instruction on the Instructions Screen <code>button.backgroundColor</code> inherits <code>colors.black.primary</code> for <code>light</code>, <code>colors.white.primary</code> for <code>dark</code> <code>UIColor</code> The background color of the button used on the Instructions Screen <code>button.textColor</code> inherits <code>colors.white.primary</code> for <code>light</code>, <code>colors.black.primary</code> for <code>dark</code> <code>UIColor</code> The text color of the button used on the Instructions Screen <p>The <code>icons</code> property can be used to provide a completely custom set of icons. The icons should be 48x48 PNGs. An example of using this property is shown below:</p> <pre><code>let customIcons = StorytellerInstructionIcons(\n    forward: UIImage(named: \"icon-forward-custom\"),\n    pause: UIImage(named: \"icon-pause-custom\"),\n    back: UIImage(named: \"icon-back-custom\"),\n    move: UIImage(named: \"icon-move-custom\")\n)\n\nvar theme = StorytellerTheme()\ntheme.light.instructions.icons = customIcons\n</code></pre> <p></p>"},{"location":"Themes/#engagement-units","title":"Engagement Units","text":"<p>The <code>engagementUnits</code> property can be used to customize properties relating to Polls and Quizzes.</p> Property Default Value Data Type Description <code>poll.answerTextColor</code> inherits <code>colors.black.primary</code> <code>UIColor</code> The text color used for Poll Answers <code>poll.percentBarColor</code> <code>#CDD0DC</code> <code>UIColor</code> The background color of the percentage bar in Poll Answers <code>poll.selectedAnswerBorderColor</code> inherits <code>colors.primary</code> <code>UIColor</code> The border color applied to the selected Poll Answer <code>poll.answeredMessageTextColor</code> inherits <code>colors.white.tertiary</code> <code>UIColor</code> The color of the vote count shown to users after they select a Poll Answer <code>poll.selectedAnswerBorderImage</code> <code>null</code> <code>UIImage?</code> A border image which can be used for the selected Poll Answer. If this is set, <code>selectedAnswerBorderColor</code> is used. <code>poll.showImageAnswerGradientOverlay</code> <code>true</code> <code>Bool</code> Shows the gradient overlay behind the text in image Poll Answers <code>poll.showPercentBarBackground</code> <code>false</code> <code>Bool</code> Adds a striped background under the percentage bar in Poll Answers <code>triviaQuiz.correctColor</code> inherits <code>colors.success</code> <code>UIColor</code> The color used to show correct answers in Trivia Quizzes <code>triviaQuiz.incorrectColor</code> inherits <code>colors.alert</code> <code>UIColor</code> The color used to show incorrect answers in Trivia Quizzes <p></p> <p></p>"},{"location":"Themes/#search","title":"Search","text":"<p>The <code>search</code> property applies customizations to suggestions, no-results, results and filters in the <code>Search</code> component. Every new Search appearance property is optional. Omitted values keep the existing iOS appearance, and <code>theme.light</code> and <code>theme.dark</code> resolve independently.</p> <p>Search text uses <code>customFont</code> with the semantic weight for each element. <code>search.heading.font</code> remains the font override for the Filters title, while Search result section headings continue to use <code>lists.title</code>. Configure the Apply Filters button through <code>search.filters.applyButton</code>; each remote or host-app field resolves independently before the compatibility default. Shared <code>buttons</code> values do not affect this Search action.</p> Property Default Value Data Type Description <code>search.backgroundColor</code> <code>colors.white.primary</code> in light mode; <code>colors.black.primary</code> in dark mode <code>UIColor?</code> Background color across Search states <code>search.backIcon</code> <code>chevron.backward</code> system image <code>UIImage?</code> Image to be used as a back icon in the Search UI <code>search.heading.font</code> inherits <code>customFont</code> <code>StorytellerFontProvider?</code> Font override for the Filters title <code>search.heading.textSize</code> <code>22</code> <code>CGFloat</code> Size of the Filters title <code>search.heading.lineHeight</code> <code>28</code> <code>CGFloat</code> Line height of the Filters title <code>search.heading.textCase</code> <code>default</code> <code>StorytellerTextCasing</code> Text case for the Filters title <code>search.heading.textColor</code> inherits <code>lists.title.textColor</code> <code>UIColor</code> Color of Filter View title <code>search.input.backgroundColor</code> existing Search field fill <code>UIColor?</code> Search field background color <code>search.input.textColor</code> current light/dark primary text color <code>UIColor?</code> Entered search text color <code>search.input.placeholderTextColor</code> system placeholder color <code>UIColor?</code> Search placeholder color <code>search.input.iconColor</code> system gray <code>UIColor?</code> Search and clear icon color <code>search.input.cornerRadius</code> existing Search field radius <code>CGFloat?</code> Search field corner radius <code>search.input.textSize</code> <code>16</code> <code>CGFloat?</code> Search field text size <code>search.input.lineHeight</code> font default <code>CGFloat?</code> Optional Search field line height <code>search.filterButton.backgroundColor</code> clear <code>UIColor?</code> Filter button background color <code>search.filterButton.iconColor</code> current light/dark primary text color <code>UIColor?</code> Filter icon color <code>search.filterButton.cornerRadius</code> <code>0</code> <code>CGFloat?</code> Filter button corner radius <code>search.suggestions.textColor</code> current light/dark primary text color <code>UIColor?</code> Suggestion text color <code>search.suggestions.iconColor</code> current light/dark primary text color <code>UIColor?</code> Suggestion icons color <code>search.suggestions.iconBackgroundColor</code> existing Search field fill <code>UIColor?</code> Magnifying-glass icon background color <code>search.suggestions.textSize</code> <code>16</code> <code>CGFloat?</code> Suggestion text size <code>search.suggestions.lineHeight</code> font default <code>CGFloat?</code> Optional suggestion line height <code>search.noResults.iconColor</code> <code>#B0B0B4</code> <code>UIColor?</code> No-results icon color <code>search.noResults.title.textColor</code> current light/dark primary text color <code>UIColor?</code> No-results title color <code>search.noResults.title.textSize</code> inherits <code>lists.title.textSize</code> <code>CGFloat?</code> No-results title size <code>search.noResults.title.lineHeight</code> inherits <code>lists.title.lineHeight</code> <code>CGFloat?</code> No-results title line height <code>search.noResults.title.textCase</code> <code>default</code> <code>StorytellerTextCasing?</code> No-results title text case <code>search.noResults.message.textColor</code> current light/dark tertiary text color <code>UIColor?</code> No-results message color <code>search.noResults.message.textSize</code> <code>16</code> <code>CGFloat?</code> No-results message size <code>search.noResults.message.lineHeight</code> font default <code>CGFloat?</code> Optional no-results message line height <code>search.noResults.message.textCase</code> <code>default</code> <code>StorytellerTextCasing?</code> No-results message text case <code>search.filters.backgroundColor</code> current light/dark background color <code>UIColor?</code> Filter sheet background color <code>search.filters.handleColor</code> system secondary color <code>UIColor?</code> Filter sheet drag-handle color <code>search.filters.sectionHeading.textColor</code> current light/dark primary text color <code>UIColor?</code> Filter section heading color <code>search.filters.sectionHeading.textSize</code> <code>16</code> <code>CGFloat?</code> Filter section heading size <code>search.filters.sectionHeading.lineHeight</code> font default <code>CGFloat?</code> Optional filter section heading line height <code>search.filters.sectionHeading.textCase</code> <code>default</code> <code>StorytellerTextCasing?</code> Filter section heading text case <code>search.filters.option.backgroundColor</code> existing Search field fill <code>UIColor?</code> Unselected filter option background color <code>search.filters.option.textColor</code> current light/dark primary text color <code>UIColor?</code> Unselected filter option text color <code>search.filters.option.borderColor</code> clear <code>UIColor?</code> Unselected filter option border color <code>search.filters.option.selectedBackgroundColor</code> existing Search field fill <code>UIColor?</code> Selected filter option background color <code>search.filters.option.selectedTextColor</code> current light/dark primary text color <code>UIColor?</code> Selected filter option text color <code>search.filters.option.selectedBorderColor</code> current light/dark primary text color <code>UIColor?</code> Selected filter option border color <code>search.filters.option.cornerRadius</code> inherits <code>primitives.cornerRadius</code> <code>CGFloat?</code> Filter option corner radius <code>search.filters.option.textSize</code> <code>16</code> <code>CGFloat?</code> Filter option text size <code>search.filters.option.lineHeight</code> font default <code>CGFloat?</code> Optional filter option line height <code>search.filters.applyButton.backgroundColor</code> black in light mode; white in dark mode <code>UIColor?</code> Apply Filters button background color <code>search.filters.applyButton.textColor</code> white in light mode; black in dark mode <code>UIColor?</code> Apply Filters button text color <code>search.filters.applyButton.textCase</code> <code>default</code> <code>StorytellerTextCasing</code> Apply Filters button text case <code>search.filters.applyButton.cornerRadius</code> inherits <code>primitives.cornerRadius</code> <code>CGFloat?</code> Apply Filters button corner radius <pre><code>var theme = StorytellerTheme()\n\ntheme.light.search.backgroundColor = UIColor.white\ntheme.light.search.input.backgroundColor = UIColor.systemGray6\ntheme.light.search.input.iconColor = UIColor.systemBlue\ntheme.light.search.filterButton.backgroundColor = UIColor.systemBlue\ntheme.light.search.filterButton.iconColor = UIColor.white\ntheme.light.search.noResults.title.textColor = UIColor.systemBlue\ntheme.light.search.filters.option.selectedBorderColor = UIColor.systemBlue\ntheme.light.search.filters.applyButton.backgroundColor = UIColor.systemBlue\ntheme.light.search.filters.applyButton.textColor = UIColor.white\n\ntheme.dark.search.backgroundColor = UIColor.black\ntheme.dark.search.input.backgroundColor = UIColor.secondarySystemBackground\ntheme.dark.search.filters.applyButton.backgroundColor = UIColor.white\ntheme.dark.search.filters.applyButton.textColor = UIColor.black\n\nStoryteller.shared.theme = theme\n</code></pre>"},{"location":"Themes/#example","title":"Example","text":"<pre><code>import StorytellerSDK\nimport UIKit\n\n// Subclassing font providers\nfinal class CustomFontProvider: StorytellerFontProvider, @unchecked Sendable {\n    override func font(weight: StorytellerFontWeight, size: CGFloat) -&gt; UIFont? {\n        switch weight {\n        case .light, .regular, .medium:\n            return UIFont(name: \"Custom Font Regular\", size: size)\n        default:\n            return UIFont(name: \"Custom Font Bold\", size: size)\n        }\n    }\n}\n\n// Setting instruction icons\nlet icons = StorytellerInstructionIcons(\n    forward: UIImage(named: \"custom-icon-forward-white\"),\n    pause: UIImage(named: \"custom-icon-pause-white\"),\n    back: UIImage(named: \"custom-icon-forward-white\"),\n    move: UIImage(named: \"custom-icon-move-white\")\n)\n\n// Creating the theme struct\nvar theme = StorytellerTheme()\n\n// Customizing theme by direct property access\ntheme.light.colors.primary = UIColor.blue\ntheme.light.instructions.headingColor = UIColor.black\ntheme.light.instructions.icons = icons\ntheme.light.customFont = CustomFontProvider()\n\n// Copying common theme properties over to the dark side\ntheme.dark = theme.light\n\n// Applying dark mode specific values\ntheme.dark.instructions.headingColor = UIColor.white\n</code></pre>"},{"location":"Themes/#home","title":"Home","text":"<p>The <code>home</code> property can be used to customize properties related to the Storyteller Home component.</p> Property Default Value Data Type Description <code>home.headerTitle.font</code> <code>nil</code> <code>StorytellerFontProvider</code> The only font that can vary from theme.font, defines the font for the heading <code>home.headerTitle.textSize</code> <code>22</code> <code>CGFloat</code> Size of the title in section <code>home.headerTitle.lineHeight</code> <code>28</code> <code>CGFloat</code> The line height of the title on in section <code>home.headerTitle.textCase</code> <code>default</code> <code>StorytellerTextCasing</code> Sets the text case for buttons throughout the UI. Possible values are <code>upper</code>, <code>lower</code> and <code>default</code> <code>home.headerTitle.textColor</code> <code>nil</code> <code>UIColor</code> Color of heading text in Storyteller Home <code>home.circularTitle.textSize</code> <code>11</code> <code>CGFloat</code> Size of the circular title in section <code>home.circularTitle.lineHeight</code> <code>13</code> <code>CGFloat</code> The line height of the circular title on in section <code>home.singletonTitle.textSize</code> <code>22</code> <code>CGFloat</code> Size of the singleton title in section <code>home.singletonTitle.lineHeight</code> <code>28</code> <code>CGFloat</code> The line height of the singleton title in section <code>home.gridTitle.textSize</code> <code>16</code> <code>CGFloat</code> Size of the grid title in section <code>home.gridTitle.lineHeight</code> <code>22</code> <code>CGFloat</code> The line height of the grid title on in section"},{"location":"Troubleshooting/","title":"Troubleshooting Storyteller on iOS","text":"<p>Start with what the app can observe. An empty component, a missing analytics event, and an Ad that was not requested may look similar on screen, but they have different owners and next checks.</p> <p>If you have not yet displayed your first Story row, follow the complete iOS Quickstart Guide first. For Apple TV, use the dedicated tvOS Guide.</p>"},{"location":"Troubleshooting/#start-with-the-observable-result","title":"Start with the Observable Result","text":"<p>Record the first unexpected result before changing the integration:</p> Observable result Check next Expected observation Likely owner Continue with <code>StorytellerSDK</code> cannot be imported, linked, or loaded Confirm the installation method, app target membership, and required Storyteller dependencies. The app builds and launches before Storyteller initialization begins. App dependency setup, or SDK packaging if the documented setup still fails Quickstart installation <code>initialize(...)</code> throws or <code>isInitialized</code> remains <code>false</code> Capture the error, then verify the API key, tenant/environment, and connectivity. A successful call returns without throwing and sets <code>isInitialized</code> to <code>true</code>. App input, tenant/environment, network, or SDK initialization Initialization fails A content load callback reports an error or <code>success == false</code> Inspect the returned error and capture related SDK logs through <code>StorytellerDelegate.log(message:)</code>. The callback distinguishes a failed request from an empty successful response. App integration, network, tenant, or SDK request path Content fails to load A list load succeeds with <code>dataCount == 0</code> Verify the content identifier, publication state, schedule/expiry, targeting inputs, and initialization-time personalization settings. A known available item from the same tenant and effective targeting context produces a positive count. App targeting or privacy configuration, tenant, CMS, or content state Content is missing or empty A list load succeeds with <code>dataCount &gt; 0</code>, but nothing is visible Check the component's constraints or frame and any visibility-affecting configuration. Giving the component a non-zero layout reveals the loaded items. App layout or presentation Content loaded but is not visible A load is requested but no completion callback arrives Confirm initialization completed successfully, the callback recipient is retained, any deferred list identifier remained unchanged, and the request is not blocked. The load reaches a success, empty, or failure callback. App lifecycle or configuration, network, or SDK request path Loading does not complete A delegate, action, or analytics callback does not arrive Identify whether it is a data-load, interaction, or analytics callback, then check its delegate lifetime and tracking gate. Another callback in the same layer establishes whether loading, interaction, or event delivery is affected. App callback setup, privacy configuration, tenant Ads setup, or SDK event delivery Callbacks or analytics events do not arrive Appearance or configuration changes have no effect Check global versus per-component configuration, the active light/dark theme, any supported remote override, and whether the component was reconfigured or reloaded where required. A deliberately visible change appears after the active host and remote precedence layers are established. App or tenant/CMS configuration, or an unsupported SDK customization Appearance or configuration does not change Ads do not appear Establish the configured Ad source, whether an eligible placement was reached, and whether the matching extension or provider made a request. The last observed boundary identifies Storyteller setup, provider delivery, or rendering as the next owner. Tenant Ads strategy, app extension, external provider, inventory, or SDK rendering Ads do not appear <p>Do not use a visual symptom alone to assign the cause. Keep the callback result, error, and last successful boundary together.</p>"},{"location":"Troubleshooting/#installation-or-initialization-fails","title":"Installation or Initialization Fails","text":""},{"location":"Troubleshooting/#the-sdk-does-not-build-or-load","title":"The SDK Does Not Build or Load","text":"<p>Follow the instructions for the installation method you actually use:</p> <ul> <li>With Swift Package Manager, add the <code>StorytellerSDK</code> product to the app target. Its Storyteller Lottie dependency resolves transitively.</li> <li>With CocoaPods, include both Storyteller spec sources, run <code>pod install</code>, and open the generated <code>.xcworkspace</code>.</li> <li>With manual frameworks, add both <code>StorytellerSDK.xcframework</code> and <code>StorytellerLottie.xcframework</code> to the app target and select Embed &amp; Sign for both.</li> <li>When using a Storyteller Ads extension, use compatible versions of the core SDK and extension and import the extension product separately.</li> </ul> <p>The expected result is that the app imports, links, and launches with the SDK before <code>initialize(...)</code> runs. If a documented clean installation fails in a minimal app, capture the package resolution or linker error and get help from Storyteller. See Quickstart installation for the complete setup.</p>"},{"location":"Troubleshooting/#initialization-fails","title":"Initialization Fails","text":"<p><code>Storyteller.shared.initialize(...)</code> is <code>async throws</code>. The clearest integration sequence is to await it and retain the original error before loading content:</p> <ol> <li>Confirm the API key identifies the intended tenant and environment.</li> <li>Confirm the current device can reach the required network services and that the app's network policy is not blocking the request.</li> <li>Inspect the error and <code>Storyteller.shared.isInitialized</code> after the call finishes.</li> </ol> <p>Initialization can throw <code>StorytellerError.networkError(Error)</code> for an unsuccessful server response. It can also surface an underlying URL, transport, or response-decoding error, so keep a general error path. After a thrown initialization call, <code>isInitialized</code> remains <code>false</code>; after success, it is <code>true</code>.</p> <p>If the same key initializes in the intended environment but one component fails later, initialization is no longer the failing boundary. Continue with Content Fails to Load.</p>"},{"location":"Troubleshooting/#content-is-blank-missing-or-still-loading","title":"Content Is Blank, Missing, or Still Loading","text":"<p>Observe the component's load callback before changing its layout or content identifiers:</p> <ul> <li>Story and Clips lists report <code>success</code>, <code>error</code>, and <code>dataCount</code> through <code>StorytellerListViewDelegate</code> or <code>StorytellerListAction</code>.</li> <li>Cards report a <code>Result&lt;Void, Error&gt;</code> through their action or delegate.</li> <li>Embedded Clips report <code>success</code> and <code>error</code> through their action or delegate.</li> </ul> <p>See Storyteller Delegates, Storyteller Cards, and Embedded Clips for the callback used by each surface.</p>"},{"location":"Troubleshooting/#content-fails-to-load","title":"Content Fails to Load","text":"<p>An error or <code>success == false</code> means the request did not produce a successful content result. It is not the same as a successful response containing no available items.</p> <ol> <li>Record the returned error and whether initialization had already succeeded.</li> <li>Capture Storyteller SDK logs through <code>StorytellerDelegate.log(message:)</code> on the strongly retained object assigned to <code>Storyteller.shared.delegate</code>. Reproduce the failure and look for the request URL, transport error, or response-parsing error that corresponds to the callback. SDK request URLs can include a hashed user ID when <code>enableRemoteViewingStore</code> is enabled, even when personalization is disabled; they can also include custom-attribute values when personalization is enabled. Redact those values before forwarding logs to a third-party service or sharing them.</li> <li>Confirm the category or collection identifier was copied from the same Storyteller tenant as the API key. When the app has separate development and production configurations, check that the key and identifier came from the same configuration.</li> </ol> <p>If initialization also fails, return to Initialization Fails. If initialization succeeds and the same request repeatedly fails with valid inputs and connectivity, retain the error and request details and get help from Storyteller.</p>"},{"location":"Troubleshooting/#content-is-missing-or-empty","title":"Content Is Missing or Empty","text":"<p>For lists, <code>success == true</code>, no error, and <code>dataCount == 0</code> establishes that the request completed but returned no items available to this component and targeting context.</p> <p>Check:</p> <ul> <li>The category or collection identifier is the intended one and belongs to the same tenant and environment as the API key.</li> <li>The content is assigned to that category or collection and is published rather than draft.</li> <li>The publication schedule has started and the content has not expired.</li> <li>When availability depends on an external user ID or custom attributes, <code>Storyteller.shared.eventTrackingOptions.enablePersonalization</code> was enabled during initialization. When it is disabled, those targeting values are omitted from supported content requests. The standard <code>userId</code> query parameter on those requests also requires <code>enableRemoteViewingStore</code>; remote-viewing requests follow the separate contract described in Privacy and Tracking.</li> <li>The external user ID, custom attributes, locale, followed-category state, and other targeting inputs make the content available.</li> <li>The selected component and configuration are intended for that content type.</li> </ul> <p>The expected control is a known, currently available item for the same targeting inputs and environment. If that appears, the original result belongs to content or targeting configuration rather than layout. See Working with Users for external IDs and targeting inputs, Privacy and Tracking for initialization-time personalization settings, and Storyteller List Views for list identifiers and configuration.</p>"},{"location":"Troubleshooting/#content-loaded-but-is-not-visible","title":"Content Loaded but Is Not Visible","text":"<p>For lists, <code>success == true</code> with <code>dataCount &gt; 0</code> establishes that content loaded. Diagnose presentation next:</p> <ul> <li>Give a UIKit row or grid valid Auto Layout constraints and a non-zero size.</li> <li>Give a SwiftUI row an appropriate non-zero frame in its parent layout.</li> <li>Confirm the component is in the visible view hierarchy and is not hidden by host-app state.</li> <li>Check configuration that changes list visibility or sizing, such as <code>visibleTiles</code>, before forcing a fixed height.</li> </ul> <p>The expected result is that the already loaded items become visible without changing the content identifier. Start from the working layouts in the iOS Quickstart Guide, then move to Storyteller List Views for advanced layouts.</p>"},{"location":"Troubleshooting/#loading-does-not-complete","title":"Loading Does Not Complete","text":"<p>Story and Clips lists and Cards can defer a load requested before initialization. This is a safeguard, not the recommended integration sequence; await initialization explicitly. A deferred Story or Clips list reload continues only when its list identifier remains unchanged while it waits. Cards wait for initialization without that identifier check.</p> <p>If the app requests a load but no completion arrives:</p> <ol> <li>Confirm <code>initialize(...)</code> eventually completed successfully. If an attempt throws, the deferred load remains pending and can continue after a later successful retry.</li> <li>For a Story or Clips list requested before initialization, confirm its identifier did not change while waiting. If it changed, the SDK abandons that deferred reload before <code>onDataLoadStarted()</code> or <code>onDataLoadComplete(...)</code>; after initialization succeeds and the deferred attempt unwinds, request a new reload with the current identifier.</li> <li>Do not issue another Story or Clips list reload while an earlier reload is in progress, including while it waits for initialization. The later call is ignored and does not produce its own <code>onDataLoadStarted()</code> or <code>onDataLoadComplete(...)</code>; wait for the earlier attempt to finish or unwind before retrying.</li> <li>Assign the delegate or action before starting the load and keep any weak delegate strongly referenced by app-owned state.</li> <li>Confirm the device remains online and use the SDK log callback and network diagnostics to inspect whether a request starts or stalls.</li> <li>Reduce the integration to one known content identifier and one component.</li> </ol> <p>The expected result is a terminal success, empty, or failure callback. If no request starts, focus on app lifecycle and configuration. If a request starts but never finishes under repeatable network conditions, capture the last observed request boundary and get help from Storyteller.</p>"},{"location":"Troubleshooting/#callbacks-or-analytics-events-do-not-arrive","title":"Callbacks or Analytics Events Do Not Arrive","text":"<p>First identify the callback layer:</p> Callback layer Primary check Expected observation List, Card, or Embedded Clips loading Assign the surface's delegate or action before loading; strongly retain weak delegates. A load-start or completion result arrives independently of analytics settings. Player interaction and app navigation Assign and strongly retain <code>Storyteller.shared.delegate</code>; the SDK property is weak. The implemented callback arrives when its corresponding interaction occurs. Analytics through <code>onUserActivityOccurred</code> Set tracking options during initialization and keep <code>enableUserActivityTracking</code> enabled. Supported non-Ad interactions produce user-activity events. Ad analytics through <code>onUserActivityOccurred</code> Also keep <code>enableAdTracking</code> enabled and reach an actual Ad opportunity or interaction. Ad events arrive only when an Ad lifecycle event occurs. <p><code>eventTrackingOptions</code> can only be selected during initialization. Reinitialize the SDK to change them. <code>enableUserActivityTracking</code> gates <code>onUserActivityOccurred</code>; disabling <code>enableAdTracking</code> additionally suppresses Ad-related analytics and omits default KVPs and <code>customKvps</code> from supported Google Ad requests.</p> <p>Tracking options govern analytics delivery; they do not determine whether a content request or provider Ad request is made. Use the callback belonging to the affected surface before changing privacy settings.</p> <p>See Integrate Analytics, Privacy and Tracking, the Analytics Event Reference, and Storyteller Delegates for the owning contracts.</p>"},{"location":"Troubleshooting/#appearance-or-configuration-does-not-change","title":"Appearance or Configuration Does Not Change","text":"<p>Check which configuration should win:</p> <ol> <li><code>Storyteller.shared.theme</code> is the global fallback theme. Set it early in the app lifecycle.</li> <li>A theme passed in a list configuration applies to that component instead of the global fallback.</li> <li>For properties supported by remote appearance settings, a feed- or collection-specific remote value overrides the tenant/global remote value, which overrides the host-supplied light or dark theme. Confirm the active CMS values before treating a host customization as an SDK gap.</li> <li>The list's <code>uiStyle</code> selects the light or dark theme branch. For a universal customization, configure <code>theme.light</code>, copy it with <code>theme.dark = theme.light</code>, then apply any dark-specific overrides.</li> <li>After changing list configuration, reconfigure and reload the UIKit view, or update and reload the SwiftUI model, as described in Storyteller List Views.</li> <li>Test one deliberately visible property on a basic component before combining multiple overrides.</li> </ol> <p>If the change works on the basic component but not the production surface, compare per-component overrides, the active appearance branch, and configuration timing. If the documented property never affects its supported surface in a minimal integration, record the property, surface, and active theme branch as a possible SDK gap.</p> <p>Use Custom Themes for supported properties and their scopes. The troubleshooting route does not imply that every visual detail is customizable.</p>"},{"location":"Troubleshooting/#ads-do-not-appear","title":"Ads Do Not Appear","text":"<p>Ads cross several ownership boundaries. Establish the configured source before debugging the provider:</p> <ul> <li>Storyteller First Party Ads are managed through the Storyteller CMS and require no additional provider integration in the app.</li> <li>GAM, AdMob, VAST, and GAM VAST Ads require the matching Storyteller extension and provider setup.</li> </ul> <p>Then identify the exact placement:</p> <ul> <li>For fullscreen Story or Clip Ads, confirm the tenant's Ads strategy enables the content type and placement you are testing. For a Clips opening pre-roll, the presentation must also opt in with <code>StorytellerClipsAdConfiguration(preRollEnabled: true)</code>.</li> <li>For a Clips bottom banner, confirm the Storyteller feed response enables the placement with <code>showBottomBannerAd == true</code>, the presentation opts in with <code>StorytellerClipsAdConfiguration(bottomBannerEnabled: true)</code>, and the active Ads module supports bottom banners. This placement is separate from the fullscreen and opening pre-roll strategy.</li> </ul> <p>Follow the branch for the configured source and placement:</p> Configured source Supported placement and next check Storyteller First Party Ads Confirm an eligible Ad is available in the CMS for a fullscreen placement, then observe whether the SDK displays it. First Party Ads do not support Clips bottom banners. GAM or AdMob Ads Confirm the app installed and configured the matching extension, then use the extension and provider diagnostics to establish whether the placement produced a provider request. A Clips bottom banner additionally requires <code>bottomBannerAdUnit</code>. VAST or GAM VAST Ads Confirm the app installed and configured the matching extension, then inspect its diagnostics for a fullscreen placement. These modules do not support Clips bottom banners. <p>Continue from the last source-specific observation:</p> <ol> <li>If a provider request starts, inspect its diagnostics or error for no-fill, trafficking, creative, consent, bidder, or network failures.</li> <li>If the CMS or provider makes an eligible Ad available but Storyteller does not display it, capture the Ad source, returned format when applicable, and subsequent SDK events or logs.</li> </ol> <p>Interpret the last successful boundary:</p> Last observation Next owner to check No eligible First Party Ad is available in the CMS for a fullscreen placement Tenant Ads strategy, cadence, First Party Ad configuration, or content state No Clips bottom-banner request The feed response's <code>showBottomBannerAd</code> value, the presentation's <code>bottomBannerEnabled</code> opt-in, module placement support, or <code>bottomBannerAdUnit</code> No provider request after an eligible extension-backed placement Tenant Ads strategy, cadence, configured source, extension setup, placement support, or SDK routing Provider request returns no-fill or an error External provider setup, inventory, trafficking, consent, bidder, or connectivity CMS or provider makes an Ad available but it cannot be mapped or rendered Returned creative/format compatibility or SDK integration/rendering <p>Tracking settings are not proof that an Ad was requested or rendered. They control analytics delivery and, for supported provider requests, which targeting values are sent; disabling them can therefore affect a line item's ability to match without directly enabling or disabling an Ad placement.</p> <p>See Ads for provider setup, placement support, and diagnostics. Provider-specific rewrites and live trafficking checks remain in the provider's own integration workflow.</p>"},{"location":"Troubleshooting/#get-help-from-storyteller","title":"Get Help from Storyteller","text":"<p>When the documented next check does not resolve the problem, share as much of the following information as is available through your normal Storyteller support channel:</p> <ul> <li>Storyteller SDK version from <code>Storyteller.shared.version</code> and the versions of any Storyteller extension modules.</li> <li>Installation method, Xcode version, iOS version, and device or simulator model.</li> <li>Affected surface: UIKit or SwiftUI, Stories, Clips, Cards, Embedded Clips, Player, or Ad placement.</li> <li>Minimal steps, expected result, actual result, and reproducibility.</li> <li>The last successful boundary and the complete callback result or error.</li> <li>Sanitized SDK logs captured through <code>StorytellerDelegate.log(message:)</code>, network or provider logs, and a screenshot or short recording when presentation is relevant. Redact hashed user IDs, custom-attribute values, and any other personal data from request URLs before forwarding the logs to a third-party service or sharing them.</li> <li>API key, tenant/environment name, and relevant content or Ad identifiers.</li> <li>Whether the same setup succeeds with a known control item, minimal component, or Showcase-equivalent integration.</li> </ul> <p>An SDK defect is more likely when the same supported configuration fails consistently with valid inputs, successful initialization, known available content or a valid provider response, and a minimal reproduction. Identifying the last successful callback or request boundary helps Storyteller distinguish that from an app, tenant, content, or provider problem.</p> <p>Storyteller API keys and content identifiers can be shared with Storyteller because they identify the tenant and content. SDK request URLs can contain hashed user IDs when <code>enableRemoteViewingStore</code> is enabled, even when personalization is disabled; they can also contain custom-attribute values when personalization is enabled. Redact those values and any other personal data before forwarding or sharing the logs. Do not include access tokens or unredacted provider credentials.</p>"},{"location":"Users/","title":"Working with Users and User customization","text":"<p>This page describes how to customize Storyteller's behaviour on a per-user basis.</p>"},{"location":"Users/#working-with-users","title":"Working with Users","text":"<p>User IDs can be used for reporting purposes, storing the read status of Clips and Stories, followed categories, user preferences and other features. By default, the Storyteller SDK creates an <code>externalId</code> for users when the SDK is first initialized. <code>externalId</code> is stored until the user uninstalls the app or until the SDK is initialized with a different <code>externalId</code>.</p> <p>However, if you have a user account system then you may wish to set your own user IDs within the Storyteller SDK.</p> <p>The <code>externalId</code> should be an identifier that is unique per user and does not change. Therefore, using something like the user's email address is not a good choice for an <code>externalId</code>, as the user may change it at some point in the future. However, using a unique UUID/GUID would be a good choice as it is guaranteed not to change over time. The SDK hashes <code>externalId</code>, <code>externalId</code> isn't stored or sent to the server in the raw form.</p>"},{"location":"Users/#setting-a-user-id","title":"Setting a User ID","text":"<p>In order to supply the <code>externalId</code> to the Storyteller SDK, call the following method:</p> <pre><code>let userInput = StorytellerUserInput(externalId: \"user-id\")\n\nTask {\n    do {\n        try await Storyteller.shared.initialize(apiKey: \"[APIKEY]\", userInput: userInput)\n    } catch {\n        // handle error\n    }\n}\n</code></pre> <p><code>apiKey</code> is a required parameter while <code>userInput</code> is optional and its default value is <code>nil</code>. If <code>userInput</code> is <code>nil</code>, the SDK will use a default autogenerated <code>externalId</code>. When calling <code>Storyteller.shared.initialize</code>, if the new <code>userInput</code> value is different from the previous one, all local data related to the previous user will be deleted.</p> <p><code>Storyteller.shared.initialize</code> should be called as soon as the value for the <code>externalId</code> is known in your app. Avoid making simultaneous <code>Storyteller.shared.initialize</code> calls, as it might result in unexpected behavior.</p> <p>Note: <code>StorytellerUserInput(externalId:)</code> is a failable initializer and returns <code>nil</code> if <code>externalId</code> is missing or blank.</p>"},{"location":"Users/#setting-the-users-locale","title":"Setting the User's Locale","text":"<p>Use an ISO 639-1 two-letter language code when available, or an ISO 639-2 three-letter language code when needed:</p> <pre><code>Storyteller.shared.user.setLocale(\"fr\")\n</code></pre> <pre><code>Storyteller.shared.user.setLocale(\"ace\")\n</code></pre> <p>The SDK lowercases the supplied value and validates it against <code>Locale.isoLanguageCodes</code>. An invalid code is ignored and leaves the previously stored locale unchanged. If your app needs a diagnostic instead of that silent no-op, validate before calling the SDK:</p> <pre><code>import Foundation\nimport StorytellerSDK\n\nfunc applyStorytellerLocale(_ locale: String) {\n    let normalizedLocale = locale.lowercased()\n    guard Locale.isoLanguageCodes.contains(normalizedLocale) else {\n        assertionFailure(\"Unsupported Storyteller locale: \\(locale)\")\n        return\n    }\n\n    Storyteller.shared.user.setLocale(normalizedLocale)\n}\n</code></pre> <p>To clear or reset the <code>locale</code> settings, simply pass <code>nil</code> as the parameter:</p> <pre><code>Storyteller.shared.user.setLocale(nil)\n</code></pre>"},{"location":"Users/#changing-users","title":"Changing Users","text":"<p>If you use login in your app and wish to allow users to logout and log back in as a new user (or proceed as an anonymous user) then when a user logs out you should call <code>initialize</code> again specifying a new <code>externalId</code>. Note that this will reset the local store of user data, for example which Pages or Clips the user has viewed.</p>"},{"location":"Users/#user-customization","title":"User Customization","text":""},{"location":"Users/#custom-attributes","title":"Custom Attributes","text":"<p>One of the features of Storyteller is the ability to set custom attributes for requests. These custom attributes can be used to provide additional information about a request, such as the user's location or the device they are using. It can be also used for audience targeting and content personalization. These custom attributes are sent with every request made by the SDK.</p>"},{"location":"Users/#how-to-use","title":"How to Use","text":""},{"location":"Users/#set-custom-attribute","title":"Set custom attribute","text":"<p>To set a custom attribute, you can use the <code>setCustomAttribute()</code> method of the <code>Storyteller.shared.user</code> object. This method takes two parameters: the key of the custom attribute and its value. For example, to set a custom attribute for the user's location, you can use the following code:</p> <pre><code>Storyteller.shared.user.setCustomAttribute(key: \"location\", value: \"New York\")\n</code></pre> <p>The Showcase app demonstrates both the UI for selecting attributes and the SDK calls that apply them: see the personalization UI in <code>AccountView</code> and the attribute wiring in <code>StorytellerService.addValue</code> and <code>StorytellerService.removeValue</code>.</p> <p>This will add a custom attribute to all requests made by the user, with the key \"location\" and the value \"New York\". You can set multiple custom attributes by calling setCustomAttribute() multiple times with different keys and values. String type values are allowed.</p>"},{"location":"Users/#set-custom-attributes","title":"Set custom attributes","text":"<p>To set multiple custom attributes at once, use the <code>setCustomAttributes()</code> method with a dictionary of string keys and values. Note that this replaces all previously stored custom attributes. For example:</p> <pre><code>Storyteller.shared.user.setCustomAttributes([\n    \"location\": \"New York\",\n    \"device_type\": \"mobile\"\n])\n</code></pre>"},{"location":"Users/#remove-custom-attribute","title":"Remove custom attribute","text":"<p>To remove custom attribute, you can use the <code>removeCustomAttribute()</code> method of the <code>Storyteller.shared.user</code> object. This method takes one parameter: the key of the custom attribute to remove. For example, to remove a custom attribute for the user's location, you can use the following code</p> <pre><code>Storyteller.shared.user.removeCustomAttribute(key: \"location\")\n</code></pre>"},{"location":"Users/#followed-categories","title":"Followed Categories","text":"<p>Storyteller provides a mechanism for managing followed categories associated with clips. Following or unfollowing a category updates the corresponding plus or tick icon in the Clips UI. The SDK supports two following models, one of which is configured in the System Admin settings for your tenant.</p>"},{"location":"Users/#app-managed-following","title":"App-Managed Following","text":"<p>Your app manages following state and handles synchronization with your systems. This gives you full control over the following logic and data storage. When interacting with Storyteller, you have the following methods at your disposal:</p>"},{"location":"Users/#update-followed-categories","title":"Update followed categories","text":"<p>To add a followed category, a list of them, or to remove a followed category, call one of the below methods by passing the respective category ID(s):</p> <pre><code>Storyteller.shared.user.addFollowedCategory(\"location\")\nStoryteller.shared.user.addFollowedCategories([\"location\", \"city\", \"country\"])\nStoryteller.shared.user.removeFollowedCategory(\"location\")\nStoryteller.shared.user.removeFollowedCategories([\"location\", \"city\"])\n</code></pre> <p>When using these methods, the SDK automatically updates the user attributes to reflect the changes in followed categories. This ensures consistency between the followed categories state and user attributes sent with requests.</p> <p>Use <code>setFollowedCategories(_:)</code> when you have the complete list of categories that should be followed and need the SDK's current followed category state to match that list exactly. This method is asynchronous, so you can wait for the SDK to update local followed category IDs and request personalization attributes before reloading Storyteller content.</p> <p>Storyteller resolves the supplied IDs before applying the replacement. IDs that are not returned by Storyteller are omitted, while the resolved subset is applied successfully. If none of the supplied IDs resolve, the followed category state is cleared. Request or response-decoding failures still throw an error and leave the previous state unchanged.</p> <pre><code>let clipsView = StorytellerClipsViewController()\n\nTask {\n    do {\n        try await Storyteller.shared.user.setFollowedCategories([\"city\", \"country\"])\n        clipsView.reloadData()\n    } catch {\n        // handle error\n    }\n}\n</code></pre>"},{"location":"Users/#react-to-followed-categories-changes-within-the-sdk","title":"React to followed categories changes within the SDK","text":"<p>Whenever a user follows or unfollow a category from within the SDK's UI (e.g. the Clips Player), the SDK triggers the following delegate method on <code>StorytellerDelegate.categoryFollowActionTaken</code> method, which allows the integrating app to synchronize its state/UI.</p> <p>See the Showcase handling of follow/unfollow callbacks in <code>StorytellerInstanceDelegate.categoryFollowActionTaken</code>.</p>"},{"location":"Users/#storyteller-managed-following","title":"Storyteller-Managed Following","text":"<p>The SDK handles following activity internally and automatically syncs with Storyteller's servers. This simplifies integration by removing the need for custom following logic in your app.</p> <p>This means that all of the above methods on <code>Storyteller.shared.user</code> are no-ops in this mode. Also the <code>categoryFollowActionTaken</code> is not called.</p>"},{"location":"Users/#common-methods","title":"Common methods","text":"<p>To check if a specific category is being followed, use <code>isCategoryFollowed(_:)</code>. To retrieve a list of all currently followed categories, read <code>followedCategories</code>.</p> <pre><code>let isFollowing: Bool = Storyteller.shared.user.isCategoryFollowed(\"location\")\nlet followedCategories: [String] = Storyteller.shared.user.followedCategories\n</code></pre> <p>To retrieve the backend-backed list of categories the current user can follow, use <code>Storyteller.shared.user.getFollowableCategories()</code>. This async method returns category metadata plus SDK-derived current followed state for each category, which is useful when building a custom followable category management UI.</p> <p>Each returned category includes a non-optional <code>id</code>, optional <code>name</code>, <code>displayTitle</code>, <code>externalId</code>, <code>type</code>, <code>thumbnailUrl</code>, and <code>placement</code>, plus SDK-derived <code>isFollowed</code>.</p> <pre><code>Task {\n    do {\n        let followableCategories = try await Storyteller.shared.user.getFollowableCategories()\n\n        for category in followableCategories.categories {\n            print(\"\\(category.name ?? \"Unnamed category\"): \\(category.isFollowed)\")\n        }\n    } catch {\n        // handle error\n    }\n}\n</code></pre> <p>Use <code>Storyteller.shared.user.followedCategories</code> and <code>Storyteller.shared.user.isCategoryFollowed(_:)</code> when you only need the SDK's current local followed category IDs. Use <code>Storyteller.shared.user.getFollowableCategories()</code> when you need renderable category data from Storyteller's backend with <code>isFollowed</code> derived from the SDK's local followed category state.</p>"},{"location":"tvOS/","title":"tvOS Guide","text":"<p>This page covers tvOS integration for Storyteller. Use the iOS guide only for iOS apps.</p>"},{"location":"tvOS/#quickstart","title":"Quickstart","text":"<p>Before adding Storyteller to your tvOS app, make sure the app has:</p> <ol> <li>A Storyteller API key for your tenant.</li> <li>An Xcode installation. We test the current SDK line with Xcode 26.2; earlier Xcode versions may also work, but they are not part of our tested configuration.</li> <li>A tvOS deployment target of 15.0 or later.</li> </ol> <p>The SDK supports Swift concurrency initialization on tvOS and is included in the same distribution channels as iOS. The iOS Showcase applications have separate build requirements that do not change the tvOS deployment floor.</p>"},{"location":"tvOS/#prerequisites","title":"Prerequisites","text":"<ul> <li>Add the API key for your tenant.</li> <li>Ensure your app target and extensions are configured for tvOS 15.0+.</li> <li>Confirm Storyteller initialization is performed before opening Storyteller views.</li> </ul>"},{"location":"tvOS/#installation","title":"Installation","text":""},{"location":"tvOS/#swift-package-manager","title":"Swift Package Manager","text":"<p>The SDK can be added as a Swift package dependency.</p> <ol> <li>In Xcode, open File &gt; Add Packages\u2026.</li> <li>Add <code>https://github.com/getstoryteller/storyteller-sdk-swift-package</code>.</li> <li>Let the package resolve <code>StorytellerLottie</code> 4.6.0 transitively; do not add upstream Airbnb Lottie separately.</li> </ol>"},{"location":"tvOS/#cocoapods","title":"CocoaPods","text":"<p>The tvOS SDK can be added with the same CocoaPods source and pod setup used on iOS.</p> <pre><code>source 'https://github.com/getstoryteller/storyteller-sdk-ios-podspec.git'\nsource 'https://github.com/getstoryteller/storyteller-lottie-ios-podspec.git'\nsource 'https://cdn.cocoapods.org/'\n\nuse_frameworks!\n\ntarget 'MyTVApp' do\n   # Pods for MyTVApp\n   pod 'StorytellerSDK'\nend\n</code></pre> <p>CocoaPods resolves <code>StorytellerLottie</code> 4.6.0 transitively from the StorytellerLottie spec repository.</p>"},{"location":"tvOS/#xcframeworks","title":"XCFrameworks","text":"<p>XCFrameworks from Storyteller releases include tvOS and can be used by tvOS apps when needed.</p> <ol> <li>Download XCFrameworks from the iOS Quickstart Guide.</li> <li>Download and unzip <code>StorytellerLottie</code> 4.6.0.</li> <li>Add both <code>StorytellerSDK.xcframework</code> and <code>StorytellerLottie.xcframework</code> to the same tvOS target. Do not add the upstream Airbnb Lottie framework: the SDK imports the <code>StorytellerLottie</code> module.</li> <li>In the tvOS target's General &gt; Frameworks, Libraries, and Embedded Content section, select Embed &amp; Sign for both XCFrameworks.</li> </ol>"},{"location":"tvOS/#sdk-initialization","title":"SDK Initialization","text":"<p>Initialize Storyteller before rendering Storyteller rows or opening Player views.</p> <pre><code>import StorytellerSDK\n\nlet userInput = StorytellerUserInput(externalId: \"user-id\")\n\nTask {\n    do {\n        try await Storyteller.shared.initialize(\n            apiKey: \"[APIKEY]\",\n            userInput: userInput\n        )\n    } catch {\n        print(\"Storyteller init failed: \\(error)\")\n    }\n}\n</code></pre>"},{"location":"tvOS/#rows-configuration-clips-and-stories","title":"Rows configuration (Clips and Stories)","text":"<p>Use separate models for Clips and Stories, then render both rows in your view hierarchy.</p> <pre><code>import StorytellerSDK\nimport SwiftUI\n\nstruct HomeView: View {\n    @State private var clipsModel: StorytellerClipsListModel\n    @State private var storiesModel: StorytellerStoriesListModel\n\n    init() {\n        let theme: StorytellerTheme = {\n            var theme = StorytellerTheme()\n            theme.light.tiles.title.textSize = 20\n            theme.dark = theme.light\n            return theme\n        }()\n\n        _clipsModel = State(\n            initialValue: StorytellerClipsListModel(\n                configuration: StorytellerClipsListConfiguration(collectionId: \"row-paging\", theme: theme)\n            )\n        )\n        _storiesModel = State(\n            initialValue: StorytellerStoriesListModel(\n                configuration: StorytellerStoriesListConfiguration(categories: [\"automation\"], theme: theme)\n            )\n        )\n    }\n\n    var body: some View {\n        ScrollView {\n            VStack(alignment: .leading, spacing: 24) {\n                Text(\"Clips\")\n                StorytellerClipsRow(model: clipsModel)\n                    .frame(height: 400)\n\n                Text(\"Stories\")\n                StorytellerStoriesRow(model: storiesModel)\n                    .frame(height: 400)\n            }\n            .padding(24)\n        }\n    }\n}\n</code></pre> <p>When the current user changes, reinitialize with the new user and call <code>reloadData()</code> on each model to refresh content.</p>"},{"location":"tvOS/#programmatic-playback","title":"Programmatic playback","text":"<p>To open a specific Story programmatically from your app UI:</p> <pre><code>import SwiftUI\nimport StorytellerSDK\n\nstruct PlaybackActionView: View {\n    var body: some View {\n        Button(\"Open latest Story\") {\n            Task {\n                do {\n                    try await Storyteller.shared.openStory(externalId: \"story-external-id\")\n                } catch {\n                    // handle playback launch error\n                    print(\"Unable to open Story: \\(error)\")\n                }\n            }\n        }\n    }\n}\n</code></pre>"},{"location":"tvOS/#tvos-specific-behavior","title":"tvOS-Specific Behavior","text":"<ul> <li>Focus and navigation are optimized for Apple TV remote interactions.</li> <li>Storyteller uses the same public integration APIs on tvOS, with platform-appropriate behavior for Player and row interactions.</li> <li>Continuous Clips playback is controlled by your CMS collection configuration. When enabled in CMS, Clips auto-advance to the next item.</li> </ul>"},{"location":"tvOS/#troubleshooting","title":"Troubleshooting","text":"<ul> <li>Initialization timing: call <code>Storyteller.shared.initialize(...)</code> before any Storyteller view is presented.</li> <li>Blank or stuck rows: confirm <code>Storyteller.shared.initialize(...)</code> succeeds and <code>reloadData()</code> is called after user/session changes.</li> <li>Playback open failures: ensure Story/Clip IDs are valid for your tenant and the app key matches the target environment.</li> <li>Focus/remote behavior: if focus does not move as expected, verify the surrounding view hierarchy and spacing, and keep enough focusable elements on screen.</li> </ul>"},{"location":"tvOS/#caveats-and-limitations","title":"Caveats and Limitations","text":"<ul> <li>Avoid custom host-level focus overrides around Storyteller rows and Player views, as they can interfere with expected focus movement.</li> </ul>"},{"location":"tvOS/#related-pages","title":"Related Pages","text":"<ul> <li>iOS Quickstart Guide</li> <li>Open Player</li> <li>StorytellerDelegate</li> <li>Theme configuration</li> <li>Analytics</li> </ul>"}]}