# Storyteller SDK Public Docs (AI bundle) This is a core, aggregated bundle of Storyteller iOS SDK public documentation for LLM agents. For a full per-page reference, map a `` marker to the concrete `llms-.txt` filename under `https://docs.getstoryteller.com/ios/ai/`. Choose a starting page by task: - New iOS integration or first visible content: `Quickstart` (complete UIKit and SwiftUI routes). - Unexpected integration, content, callback, configuration, or Ad result: `Troubleshooting`. - Add a Storyteller experience: `Storyteller List Views`, `Storyteller Home`, `Embedded Clips`, or `Cards`. - Configure identity, privacy, appearance, analytics, Ads, or navigation: use the corresponding feature guide. - Look up callbacks or less common SDK entry points: `Storyteller Delegate`, `Storyteller Module`, or `Additional Methods`. Notes: - This aggregate bundle intentionally excludes `Analytics` and `Release Notes` (Changelog) to keep the core context smaller. - Use `https://docs.getstoryteller.com/ios/ai/llms-analytics.txt` and `https://docs.getstoryteller.com/ios/ai/llms-changelog.txt` when needed. # iOS Quickstart Guide URL: /Quickstart/ ## Task You are helping an engineer get the first Storyteller content on screen in a native iOS app. Keep initialization, content, and layout failures distinct. Use the complete UIKit or SwiftUI journey and point advanced integrations to the detailed component docs and Showcase app. ## Metadata - Slug: `quickstart` - Source: `public-docs/Quickstart.md` - Audience: First-time iOS SDK integrators - Platforms: `iOS` - Related: `StorytellerListViews.md`, `Users.md`, `PrivacyAndTracking.md`, `Themes.md` ## Overview - The current SDK supports an iOS 13.0+ deployment target and is tested with Xcode 26.2. Earlier Xcode versions may also work, but they are not part of our tested configuration. The private Showcase apps target iOS 16 and have separate build settings; see `https://docs.getstoryteller.com/ios/#sdk-and-showcase-requirements`. - Integration also requires a tenant API key, a category identifier from the same tenant/environment, a stable non-PII user ID, and at least one currently published Story in that category. - Linked Showcase implementations require separate access to the private customer repository through an authorised GitHub account. - Swift Package Manager is the recommended installation path. CocoaPods and manual XCFramework instructions remain available. - Both UIKit and SwiftUI examples await `Storyteller.shared.initialize(...)` before configuring or loading content. - The UIKit example gives `StorytellerStoriesRowView` explicit Auto Layout constraints and a height. - The SwiftUI example creates `StorytellerStoriesListModel` only after initialization and gives `StorytellerStoriesRow` an explicit height. - Both examples map callbacks into visible initializing, loading, empty, failure, and success states. - The Quickstart stops after first success and links to the existing richer Showcase implementations rather than introducing another sample app. ## When To Use - You are integrating Storyteller into a native iOS app for the first time. - An existing integration displays a blank list and you need to isolate initialization, content, or layout. - You need complete, compile-checked UIKit or SwiftUI code before moving to advanced list configuration. ## Prerequisites - Obtain `` and `` from your Storyteller contact or existing tenant content setup. - Confirm that the API key and category belong to the same tenant and environment. - Publish at least one Story in the category; draft, scheduled, expired, or otherwise unavailable content does not appear. - Replace `` with a stable, non-personally-identifiable user identifier. - If you plan to use the linked implementation examples, confirm your authorised GitHub account can access the private iOS Showcase source; see `https://docs.getstoryteller.com/ios/#showcase-source-access`. ## Integration Steps 1. Install `StorytellerSDK`; prefer Swift Package Manager. 2. Replace ``, ``, and ``. 3. Await `Storyteller.shared.initialize(...)` and handle any thrown error. 4. Configure a Stories list with `StorytellerStoriesListConfiguration(categories:)`. 5. Give the row a non-zero layout. 6. Load the row and observe `onDataLoadComplete` or `StorytellerListAction.onDataLoadComplete`. 7. Treat `dataCount > 0` as first-content success, zero items as an empty/content-configuration result, and a returned error as a load failure. ## API Cheat Sheet - `Storyteller.shared.initialize(apiKey:userInput:eventTrackingOptions:)` - Main-actor, `async throws` initialization boundary. - `isInitialized` becomes `true` only after success and resets to `false` when initialization starts again. - Stable public `StorytellerError` cases are `networkError(Error)`, `contentNotFound(String)`, and `wrongInputData`; retain a general `catch` path for underlying transport or decoding errors. - `StorytellerUserInput(externalId:)` - Identifies the user with a stable, non-personally-identifiable external ID. - `StorytellerStoriesListConfiguration(categories:)` - Configures a Stories row or grid for category identifiers in the initialized tenant. - `StorytellerStoriesRowView` - UIKit surface. Assign a delegate, configure it, give it a non-zero layout, then call `reloadData()`. - `StorytellerStoriesListModel` and `StorytellerStoriesRow` - SwiftUI model and surface. Inserting the configured model-backed row triggers its initial load. - `onDataLoadComplete(success:error:dataCount:)` - Distinguishes a failed request, an empty successful response, and loaded content. ## Installation Notes - Swift Package Manager URL: `https://github.com/getstoryteller/storyteller-sdk-swift-package`. - Swift Package Manager resolves `StorytellerLottie` 4.6.0 transitively, so it does not need to be added separately. - CocoaPods requires the Storyteller SDK spec source, StorytellerLottie spec source, and CocoaPods CDN; run `pod install`, then open the generated `.xcworkspace`. StorytellerLottie resolves transitively. - Manual installation requires both `StorytellerSDK.xcframework` and `StorytellerLottie.xcframework`, both set to **Embed & Sign**. - Upstream Airbnb Lottie can coexist with the namespaced `StorytellerLottie` dependency, but it does not replace it. ## Initialization-Order Contract - The documented path always awaits initialization before requesting content. - The SDK currently defers an early list reload until initialization succeeds when the list identifier remains unchanged. - A failed initialization attempt leaves the reload pending; a later successful attempt allows it to continue when the identifier still matches. Treat this behavior as a safeguard, not the normal integration sequence. ## Diagnostics - Initialization message: verify API key, tenant/environment, connectivity, and the underlying error. - Load error: initialization succeeded; inspect the content request error and Xcode logs. - Successful zero count: verify category ID, published/scheduled/expired state, and targeting for the current user. - Positive count but no visible tiles: verify UIKit constraints or SwiftUI frame height. - Indefinite loading: verify initialization sequencing, callback lifetime, connectivity, and logs. ## Cross-References - List configuration: `StorytellerListViews.md` - User identity and personalization: `Users.md` - Production tracking choices: `PrivacyAndTracking.md` - Styling: `Themes.md` - SwiftUI list model: https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/main/ShowcaseApp/Views/Home/Components/StorytellerItemViewModel.swift#L9 - SwiftUI action handling: https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/main/ShowcaseApp/Views/Home/Components/StorytellerItemView.swift#L10 - UIKit list example: https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/cocoapods/StorytellerSampleApp/Components/MultipleLists/MultipleListsViewController.swift#L11 # Troubleshooting Storyteller on iOS URL: /Troubleshooting/ ## Task You are helping an iOS engineer diagnose a Storyteller integration. Begin with the first observable result and preserve the distinction between app integration, tenant or CMS configuration, content state, an external provider, and a likely SDK defect. Give one bounded next check, the expected observation, and the detailed owning guide instead of guessing from the visual symptom. ## Metadata - Slug: `troubleshooting` - Source: `public-docs/Troubleshooting.md` - Audience: iOS SDK integrators diagnosing an unexpected result - Platforms: `iOS` - Related: `Quickstart.md`, `StorytellerDelegate.md`, `PrivacyAndTracking.md`, `Themes.md`, `Ads.md` ## Overview - A build or import failure must be resolved at the installation boundary before initialization can be diagnosed. - A thrown initialization call leaves `Storyteller.shared.isInitialized == false`; a successful call sets it to `true`. - List callbacks distinguish request failure, successful empty content, loaded content hidden by presentation, and a request that did not complete; targeted empty content must also account for initialization-time personalization settings. - Cards and Embedded Clips expose their own completion shapes, so use the callback belonging to the affected surface. - `Storyteller.shared.delegate` and component delegates declared as weak must be strongly retained by the app. - `StorytellerDelegate.log(message:)` is the public way to capture SDK error and informational logs in debug or release builds; request URLs can include a hashed user ID when remote viewing is enabled and custom-attribute values when personalization is enabled, so both must be redacted before forwarding or sharing the logs. - Data-load callbacks, Player interaction callbacks, and analytics delivery are separate layers. - Theme troubleshooting must account for global versus per-component configuration, feed/collection and tenant remote precedence for supported fields, active light/dark branches, and component reload behavior. - Missing fullscreen Ads must be traced through tenant strategy, eligible placement, the First Party CMS or matching extension, provider response when applicable, and SDK rendering. Clips bottom banners instead require `showBottomBannerAd` in the feed response, local presentation opt-in, and a module that supports the placement. - When further help is needed, share as much reproducible evidence as is available through the normal Storyteller support channel. ## Diagnostic Flow 1. Record the first unexpected callback, error, count, or visible result before changing the integration. 2. Confirm the SDK builds and initialization succeeds before diagnosing content loading. A deferred Story or Clips list reload continues only if its identifier remains unchanged, and another reload requested while one is in progress is ignored; Cards do not use the identifier guard. 3. Use the surface's load callback to distinguish a failed request from a successful empty result, then check the initialization-time personalization settings when targeting depends on user context. 4. Treat `dataCount > 0` with no visible list as a presentation problem before changing content identifiers. 5. For a missing callback, identify whether it belongs to loading, Player interaction, or analytics. 6. For appearance, establish any supported feed/collection or tenant remote override before testing one visible property in the active global or per-component host theme branch. 7. For Ads, identify the exact placement before branching by source. Fullscreen placements use the tenant strategy; Clips bottom banners require `showBottomBannerAd`, `bottomBannerEnabled`, and a supporting module. 8. When the documented checks do not resolve the problem, capture SDK logs through `StorytellerDelegate.log(message:)`, redact personal targeting values from request URLs, and record the expected observation and last successful boundary before asking Storyteller for help. ## Key Observations - Initialization can throw `StorytellerError.networkError(Error)` for an unsuccessful server response and may also surface underlying URL, transport, or decoding errors. - A list result with `success == true`, no error, and `dataCount == 0` points to identifiers, publication state, schedule/expiry, targeting inputs, or personalization/privacy configuration rather than layout. - A list result with `success == true` and `dataCount > 0` proves content loaded; check UIKit constraints, SwiftUI frame, host visibility, and sizing configuration. - Story and Clips lists and Cards can defer loads requested before initialization. A failed initialization attempt leaves the load pending until a later attempt succeeds, but a Story or Clips list then abandons its deferred reload if the list identifier changed while waiting. Another Story or Clips reload requested while one is already in progress is ignored and has no callbacks of its own. - `eventTrackingOptions` are selected during initialization. `enableUserActivityTracking` gates `onUserActivityOccurred`; `enableAdTracking` additionally gates Ad analytics and the default KVPs and `customKvps` on supported Google Ad requests. - For supported remote fields, feed/collection values override tenant/global values, which override the host theme. Configure `theme.light`, copy it with `theme.dark = theme.light`, and then apply dark-specific overrides when most host customization should be shared. - A Clips bottom banner requires `showBottomBannerAd == true` in the feed response, the presentation to set `bottomBannerEnabled`, and a supporting module. First Party, VAST, and GAM VAST Ads do not support this placement. ## Getting Help - Storyteller SDK and extension-module versions. - Installation method, Xcode version, iOS version, and device or simulator. - Affected UIKit or SwiftUI surface and content or Ad placement. - Minimal steps, expected and actual results, reproducibility, callback result, and complete error. - Sanitized SDK logs captured through `StorytellerDelegate.log(message:)`, with hashed user IDs, custom-attribute values, and other personal data removed from request URLs before the logs are forwarded or shared. - API key, tenant/environment, content or Ad identifiers, and the result of a known control item or minimal component. - API keys and content identifiers can be shared with Storyteller; never include access tokens, personal user data, or unredacted provider credentials. - A repeatable failure with valid inputs, successful initialization, known available content or a valid provider response, and a minimal reproduction is stronger evidence of an SDK defect. ## Cross-References - First integration and blank-list diagnosis: `Quickstart.md` - Load and Player callbacks: `StorytellerDelegate.md`, `Cards.md`, `EmbeddedClips.md` - User identity and targeting: `Users.md` - Tracking gates and event payloads: `PrivacyAndTracking.md`, `Analytics.md` - Theme scope and supported properties: `Themes.md`, `StorytellerListViews.md` - Ad sources, placements, and diagnostics: `Ads.md` # Migrating to version 11 URL: /MigrationGuideV11/ ## Task You are helping an engineer integrate Storyteller. Use this documentation to answer questions, propose improvements, and point out missing details. Be precise and call out assumptions. ## Metadata - Slug: `migration-guide-v11` - Source: `public-docs/MigrationGuideV11.md` - Audience: iOS SDK integrators - Platforms: `iOS` - Related: `PrivacyAndTracking.md`, `Changelog.md`, `StorytellerDelegate.md`, `StorytellerListViews.md` ## Overview - Version 11 replaces static API access with the shared instance pattern (`Storyteller.shared`). - Public SDK types are renamed to use the `Storyteller` prefix. - Callback-based public APIs move to Swift concurrency with `async/await`. - `eventTrackingOptions` must now be provided during initialization instead of being updated later. - SwiftUI grids now require `isScrollable` to be passed explicitly. - `StorytellerListViewDelegate.onTileTapped` now passes `StorytellerTileType` instead of a raw ID. ## When To Use - You are upgrading an integration from Storyteller SDK 10.x.x to 11.x.x. - You see compiler errors due to renamed types, removed static APIs, or callback-based APIs. ## Integration Steps 1. Replace static API usage with `Storyteller.shared`. 2. Update renamed public types (prefix changes). 3. Convert callback APIs to `async/await` (`Task { ... }` + `try await`). 4. Update analytics/tracking configuration: set `eventTrackingOptions` during initialization only. 5. Update SwiftUI grid calls to pass `isScrollable` explicitly. 6. Update `StorytellerListViewDelegate.onTileTapped` implementation to handle `StorytellerTileType`. ## API Cheat Sheet ### Shared instance pattern - Before: `Storyteller.` - After: `Storyteller.shared.` Example: ```swift // Before (10.x.x): Storyteller.delegate = myDelegate // After (11.x.x): Storyteller.shared.delegate = myDelegate ``` ### Type renames (10 → 11) | Old Name | New Name | |----------|----------| | `UserInput` | `StorytellerUserInput` | | `ClipCollectionConfiguration` | `StorytellerClipCollectionConfiguration` | | `Placement` | `StorytellerPlacement` | | `Category` | `StorytellerCategory` | | `CategoryDetail` | `StorytellerCategoryDetail` | | `CurrentCategoryData` | `StorytellerCurrentCategoryData` | | `UserActivity` | `StorytellerUserActivity` | | `UserActivityData` | `StorytellerUserActivityData` | | `CodableIgnored` | `StorytellerCodableIgnored` | | `Alignment` | `StorytellerAlignment` | | `FontProvider` | `StorytellerFontProvider` | | `TextCasing` | `StorytellerTextCasing` | | `PlayerIcons` | `StorytellerPlayerIcons` | | `InstructionIcons` | `StorytellerInstructionIcons` | ### Async functions (now `async` in v11) - `initialize(apiKey:userInput:eventTrackingOptions:)` - `dismissPlayer(animated:dismissReason:)` - `openDeepLink(url:)` - `openStory(id:openReason:)` - `openStory(externalId:openReason:)` - `openPage(id:openReason:)` - `openCategory(category:openReason:)` - `openCollection(configuration:openReason:)` - `openClipByExternalId(collectionId:externalId:openReason:)` - `openSheet(id:)` - `getStoriesCount(for:)` - `getClipsCount(for:)` - `openSearch()` ## Examples Callback init → `async/await`: ```swift // Before (10.x.x): Storyteller.initialize( apiKey: "your-api-key", onComplete: { print("SDK initialized successfully") }, onError: { error in print("Initialization failed: \(error)") } ) // After (11.x.x): Task { do { try await Storyteller.shared.initialize(apiKey: "your-api-key") print("SDK initialized successfully") } catch { print("Initialization failed: \(error)") } } ``` `eventTrackingOptions` can only be set during initialization: ```swift // Initialize SDK Storyteller.initialize( apiKey: "your-api-key", onComplete: { print("SDK initialized successfully") }, onError: { error in print("Initialization failed: \(error)") } ) // Later in the code, modify tracking options Storyteller.eventTrackingOptions = StorytellerEventTrackingOptions( enablePersonalization: true, enableStorytellerTracking: true, enableUserActivityTracking: true, enableAdTracking: true, enableFullVideoAnalytics: true, enableRemoteViewingStore: true, disabledFunctionalFeatures: [] ) ``` ```swift // Set tracking options during initialization let trackingOptions = StorytellerEventTrackingOptions( enablePersonalization: true, enableStorytellerTracking: true, enableUserActivityTracking: true, enableAdTracking: true, enableFullVideoAnalytics: true, enableRemoteViewingStore: true, disabledFunctionalFeatures: [] ) let userInput = StorytellerUserInput(externalId: "user-id") Task { try await Storyteller.shared.initialize( apiKey: "your-api-key", userInput: userInput, eventTrackingOptions: trackingOptions ) } ``` SwiftUI grids require `isScrollable` explicitly: ```swift // Before (10.x.x): // isScrollable defaulted to false StorytellerStoriesGrid(model: storiesModel) StorytellerClipsGrid(model: clipsModel) // After (11.x.x): // isScrollable must be explicitly provided StorytellerStoriesGrid(isScrollable: false, model: storiesModel) StorytellerClipsGrid(isScrollable: false, model: clipsModel) ``` `StorytellerListViewDelegate.onTileTapped` now uses `StorytellerTileType`: ```swift // Before (10.x.x): extension MyViewController: StorytellerListViewDelegate { func onTileTapped(id: String) { print("Tapped tile with ID: \(id)") } } // After (11.x.x): extension MyViewController: StorytellerListViewDelegate { func onTileTapped(type: StorytellerTileType) { switch type { case .clip(let clipId, let collectionId, let categories): print("Tapped clip: \(clipId) in collection: \(collectionId), categories: \(categories)") case .story(let storyId, let categories): print("Tapped story: \(storyId), categories: \(categories)") } } } ``` ## Pitfalls / Notes - To change tracking options after initialization, you must reinitialize the SDK (see `PrivacyAndTracking.md`). - Some theme properties were removed and are now configured in the CMS: - `tiles.title.show` - `engagement.poll.showVoteCount` ## Cross-References - Shared instance pattern in Showcase: https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/main/ShowcaseApp/ShowcaseApp.swift#L95 - Async/await initialization (Showcase): https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/main/ShowcaseApp/Storyteller/StorytellerService.swift#L36 - SwiftUI grids usage (Showcase): https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/main/ShowcaseApp/Views/Home/Components/StorytellerLists.swift#L11 - onTileTapped handling (Showcase): https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/main/ShowcaseApp/Views/Home/Components/StorytellerItemView.swift#L58 # tvOS Guide URL: /tvOS/ ## Task You are helping an engineer integrate Storyteller on tvOS. Use this documentation to answer questions, propose improvements, and point out missing details. Be precise and call out assumptions. ## Metadata - Slug: `tv-os` - Source: `public-docs/tvOS.md` - Audience: tvOS SDK integrators - Platforms: `tvOS` - Related: `Quickstart.md`, `OpenPlayer.md`, `StorytellerDelegate.md`, `Themes.md`, `Analytics.md` ## Overview - The Storyteller SDK supports tvOS with a dedicated guide for setup and caveats. - The current SDK supports tvOS 15.0+ and is tested with Xcode 26.2. Earlier Xcode versions may also work, but they are not part of our tested configuration. Integration also requires a valid API key. The iOS Showcase apps have separate build settings that do not change this deployment floor; see `https://docs.getstoryteller.com/ios/#sdk-and-showcase-requirements`. - Initialization and API usage follow the shared `Storyteller.shared` pattern used by iOS, using async initialization. - The SDK includes dedicated tvOS views and behavior for remote-control navigation and focus. ## When To Use - You are integrating Storyteller for a tvOS application. - You need an onboarding path distinct from iOS setup. - You need to validate what is and is not available on tvOS before shipping. ## Integration Steps 1. Confirm deployment target is tvOS 15.0+. 2. Install the SDK with Swift Package Manager, CocoaPods, or XCFrameworks. 3. Initialize Storyteller early in app startup. 4. Add Storyteller rows/views and refresh data as normal for your host app lifecycle. 5. Verify tvOS focus and row behavior in a tvOS simulator or device. ### Installation - Swift Package Manager: `https://github.com/getstoryteller/storyteller-sdk-swift-package`; it resolves `StorytellerLottie` 4.6.0 transitively. - CocoaPods: ```ruby source 'https://github.com/getstoryteller/storyteller-sdk-ios-podspec.git' source 'https://github.com/getstoryteller/storyteller-lottie-ios-podspec.git' source 'https://cdn.cocoapods.org/' use_frameworks! target 'MyTVApp' do # Pods for MyTVApp pod 'StorytellerSDK' end ``` - XCFrameworks: download the SDK plus `StorytellerLottie` 4.6.0, add both XCFrameworks to the same tvOS target, and set both to `Embed & Sign`. Do not add upstream Airbnb Lottie. ## API Cheat Sheet - Initialize with `Storyteller.shared.initialize(apiKey:userInput:eventTrackingOptions:)` in your startup flow. - Keep initialization on the main startup path before opening Player/list views. - Use `StorytellerUserInput(externalId:)` for user scoping. ## Examples - Initialize with `Storyteller.shared.initialize(apiKey:userInput:)` before rendering tvOS Storyteller views. - Configure tvOS themes and pass list models built from: - `StorytellerClipsListModel(configuration: StorytellerClipsListConfiguration(...))` - `StorytellerStoriesListModel(configuration: StorytellerStoriesListConfiguration(...))` - Render rows with `StorytellerClipsRow(model:)` and `StorytellerStoriesRow(model:)`. - On identity change, reinitialize the SDK with the new user and reload row models (`reloadData()`). - DemoTV references: - `DemoTV/HomeView.swift` for row-model configuration and post-reset `reloadData()`. ### Playback Integration - Use `Task` + `Storyteller.shared.openStory(externalId:)` to launch Storyteller playback from custom UI controls. - Use Clip/Page/Story open APIs from the public `Storyteller.shared` API for app-owned transition handling when you need custom navigation logic. ## Pitfalls / Notes - Ensure `initialize` runs before presenting any Storyteller row/view. - If rows are empty, confirm initialization success and model reload after user/session changes. - For playback failures, verify IDs and tenant/API key pairing. - For focus/remote navigation issues, validate the surrounding focusable view structure. ### tvOS Behavior Notes - Focus and navigation are optimized for Apple TV remote interactions. - Storyteller uses the same public integration APIs on tvOS, with platform-appropriate behavior for Player and row interactions. - Continuous Clips playback is controlled by CMS collection configuration; when enabled, Clips auto-advance to the next item. - Validate focus interactions and remote-control navigation in your host app flow. ### Caveats - Avoid custom host-level focus overrides around Storyteller rows and Player views, as they can interfere with expected remote navigation. ## Cross-References - `Quickstart.md` - `OpenPlayer.md` - `StorytellerDelegate.md` - `Themes.md` - `Analytics.md` # Working with Users URL: /Users/ ## Task You are helping an engineer integrate Storyteller. Use this documentation to answer questions, propose improvements, and point out missing details. Be precise and call out assumptions. ## Metadata - Slug: `users` - Source: `public-docs/Users.md` - Audience: iOS SDK integrators - Platforms: `iOS` - Related: `PrivacyAndTracking.md`, `StorytellerDelegate.md`, `Analytics.md` ## Overview - Storyteller identifies users via `externalId` (autogenerated by default, or supplied via `StorytellerUserInput(externalId:)`). - Use a stable, unique identifier (UUID/GUID recommended); avoid mutable identifiers like email. - `externalId` is hashed; it is not stored or sent to the server in raw form. - Initializing with a different `externalId` deletes local data for the previous user. - Avoid making simultaneous `Storyteller.shared.initialize` calls. - `StorytellerUserInput(externalId:)` is failable and returns `nil` for missing/blank IDs. - User customization includes locale, custom attributes, and followed categories (app-managed vs Storyteller-managed), plus backend-backed followable category metadata for custom management UIs. ## When To Use - You have a login/account system and want to bind Storyteller state to your user IDs. - You need user-specific personalization via custom attributes. - You need category-following state integrated with your app UI. - You need to render a custom followable category management UI with category metadata and current followed state. ## Integration Steps 1. Decide whether to use an app-provided `externalId` or the SDK’s autogenerated ID. 2. Initialize Storyteller with `StorytellerUserInput(externalId:)` as soon as the ID is known. 3. Set locale (optional) via `Storyteller.shared.user.setLocale(...)`. 4. Manage custom attributes via `Storyteller.shared.user` helpers. 5. If using app-managed following, update followed categories via `Storyteller.shared.user` and listen for `StorytellerDelegate.categoryFollowActionTaken(...)`. 6. If building a custom category management UI, fetch backend-backed followable category data via `Storyteller.shared.user.getFollowableCategories()`. ## API Cheat Sheet ### User identity - `StorytellerUserInput(externalId:)` - Failable initializer; returns `nil` if the external ID is missing or blank. - `Storyteller.shared.initialize(apiKey:userInput:eventTrackingOptions:)` - `userInput` is optional (`nil` means the SDK uses an autogenerated `externalId`). - If the new `userInput` differs from the previous one, local data for the previous user is deleted. ### Locale - `Storyteller.shared.user.setLocale(_ locale: String?)` - Set locale using ISO 639-1 (two-letter) if available, otherwise ISO 639-2 (three-letter). - The SDK lowercases and validates the value against `Locale.isoLanguageCodes`; invalid values are ignored and leave the stored locale unchanged. - Pass `nil` to clear/reset. ### Custom attributes - `Storyteller.shared.user.setCustomAttribute(key:value:)` - Sets a single custom attribute for all SDK requests. - `Storyteller.shared.user.setCustomAttributes(_:)` - Sets multiple attributes at once; replaces all previously stored custom attributes. - `Storyteller.shared.user.removeCustomAttribute(key:)` - Removes an attribute by key. ### Followed categories - App-managed following - `addFollowedCategory(_:)`, `addFollowedCategories(_:)`, `removeFollowedCategory(_:)`, `removeFollowedCategories(_:)` - `setFollowedCategories(_:)` asynchronously replaces the full SDK followed-category set when you have the complete list that should be followed. IDs Storyteller cannot resolve are omitted; an all-unresolved replacement clears the state, while request or response-decoding failures preserve the previous state and throw. - The SDK updates user attributes to keep followed categories consistent with attributes sent on requests. - When a user follows/unfollows inside the SDK UI, `StorytellerDelegate.categoryFollowActionTaken(category:isFollowing:)` is called (see `StorytellerDelegate.md#categoryfollowactiontaken`). - Storyteller-managed following - Following is handled internally and synced with Storyteller servers; the above `Storyteller.shared.user` following methods are no-ops and `categoryFollowActionTaken` is not called. - Common - `isCategoryFollowed(_:)` checks a category ID. - `followedCategories` returns all currently followed category IDs. - `Storyteller.shared.user.getFollowableCategories()` asynchronously returns renderable followable category metadata from Storyteller's backend plus SDK-derived current followed state. - returned categories include a non-optional `id`, optional `name`, `displayTitle`, `externalId`, `type`, `thumbnailUrl`, and `placement`, plus `isFollowed`. ## Examples Initialize with a specific user: ```swift let userInput = StorytellerUserInput(externalId: "user-id") Task { do { try await Storyteller.shared.initialize(apiKey: "[APIKEY]", userInput: userInput) } catch { // handle error } } ``` Validate a locale when the host app needs a diagnostic rather than the SDK's silent no-op: ```swift import Foundation import StorytellerSDK func applyStorytellerLocale(_ locale: String) { let normalizedLocale = locale.lowercased() guard Locale.isoLanguageCodes.contains(normalizedLocale) else { assertionFailure("Unsupported Storyteller locale: \(locale)") return } Storyteller.shared.user.setLocale(normalizedLocale) } ``` Set multiple custom attributes: ```swift Storyteller.shared.user.setCustomAttributes([ "location": "New York", "device_type": "mobile" ]) ``` Update followed categories: ```swift Storyteller.shared.user.addFollowedCategory("location") Storyteller.shared.user.addFollowedCategories(["location", "city", "country"]) Storyteller.shared.user.removeFollowedCategory("location") Storyteller.shared.user.removeFollowedCategories(["location", "city"]) ``` Replace followed categories with a complete list: ```swift Task { do { try await Storyteller.shared.user.setFollowedCategories(["city", "country"]) clipsView.reloadData() } catch { // handle error } } ``` Fetch followable categories for a custom UI: ```swift Task { do { let followableCategories = try await Storyteller.shared.user.getFollowableCategories() for category in followableCategories.categories { print("\(category.name): \(category.isFollowed)") } } catch { // handle error } } ``` ## Pitfalls / Notes - `Storyteller.shared.initialize` should be called as soon as the `externalId` is known in your app. - Reinitializing with a different `externalId` resets local data for the previous user. - Avoid making simultaneous `Storyteller.shared.initialize` calls. - Invalid locale codes are ignored; validate in the host app if you need to surface an error. - In Storyteller-managed following mode, the `Storyteller.shared.user` following methods are no-ops and `categoryFollowActionTaken` is not called. - Use `Storyteller.shared.user.followedCategories` / `Storyteller.shared.user.isCategoryFollowed(_:)` for local followed IDs; use `Storyteller.shared.user.getFollowableCategories()` when you need backend-backed category metadata and followed state. ## Cross-References - Personalization UI: https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/main/ShowcaseApp/Views/Account/AccountView.swift#L8 - Attribute wiring: https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/main/ShowcaseApp/Storyteller/StorytellerService.swift#L141 - Attribute removal: https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/main/ShowcaseApp/Storyteller/StorytellerService.swift#L158 - Follow callback handling: https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/main/ShowcaseApp/Storyteller/StorytellerInstanceDelegate.swift#L89 # Privacy and Tracking URL: /PrivacyAndTracking/ ## Task You are helping an engineer integrate Storyteller. Use this documentation to answer questions, propose improvements, and point out missing details. Be precise and call out assumptions. ## Metadata - Slug: `privacy-and-tracking` - Source: `public-docs/PrivacyAndTracking.md` - Audience: iOS SDK integrators - Platforms: `iOS` - Related: `Analytics.md`, `StorytellerDelegate.md`, `Ads.md`, `Users.md` ## Overview - `eventTrackingOptions` customizes Storyteller analytics and tracking behavior based on privacy choices. - Configure it by passing a `StorytellerEventTrackingOptions` into `Storyteller.shared.initialize(...)`. - Default behavior is `.enableAll` (all tracking options enabled). - Tracking options can be read after init via `Storyteller.shared.eventTrackingOptions`, but cannot be modified at runtime. - To change tracking options after initialization, you must reinitialize the SDK with new options. - User attributes require `enablePersonalization`; supported personalization requests include the standard `userId` query parameter only when both `enablePersonalization` and `enableRemoteViewingStore` are enabled. - Remote-viewing requests are separate and can include the stored hashed user ID whenever `enableRemoteViewingStore` is enabled, even when personalization is disabled. - `onUserActivityOccurred()` is invoked only when `enableUserActivityTracking` is enabled. Ad loading callbacks such as `getAd` and `getBottomBannerAd` are separate integration APIs. - `disabledFunctionalFeatures` disables specific functional behaviors for privacy compliance (the SDK behaves as if those features are disabled from the server). ## When To Use - You need to honor consent/opt-out choices (personalization, analytics, ads, or activity tracking). - You need a privacy-enhanced mode where user IDs are not stored or sent to backend services (VPPA-related concerns). - You want to disable specific persistence behaviors (read status, viewed status, poll/quiz persistence, likes/shares persistence) for compliance reasons. ## Integration Steps 1. Decide your tracking behavior based on user consent. 2. Create a `StorytellerEventTrackingOptions` value. 3. Pass it into `Storyteller.shared.initialize(apiKey:userInput:eventTrackingOptions:)`. 4. If consent changes later, reinitialize with new options. ## API Cheat Sheet - `Storyteller.shared.initialize(apiKey:userInput:eventTrackingOptions:)` - `eventTrackingOptions` can only be set during SDK initialization. - `Storyteller.shared.eventTrackingOptions` - Publicly readable after initialization; cannot be modified at runtime. ### `StorytellerEventTrackingOptions` - `enablePersonalization` - When enabled, supported Storyteller requests include user attributes to personalize returned content. - Supported personalization requests include the standard `userId` query parameter only when `enableRemoteViewingStore` is also enabled. - `enableStorytellerTracking` - When enabled, Storyteller records analytics events on Storyteller servers. - Some events are necessary for user functionality and may still be transmitted (but not stored) even when this is disabled. - `enableUserActivityTracking` - When enabled, Storyteller calls the delegate method `onUserActivityOccurred()` so the integrating app can record events in its own analytics system. - When disabled, that callback is not invoked. - `enableAdTracking` - When disabled, ad-related events are not tracked through `onUserActivityOccurred()` and not tracked on Storyteller servers. - GAM requests include only necessary fields (for example, Ad Unit Id and Custom Template Ids). - `enableFullVideoAnalytics` - When disabled, sensitive video event data (IDs/titles for Stories, Pages, Clips, Cards, and related display fields) is not included in `onUserActivityOccurred()`. - `enableRemoteViewingStore` - When enabled, separate requests used to retrieve and record viewing activity and synchronize followed categories can include the stored hashed user ID independently of personalization. - When disabled, user IDs are never stored or sent to backend services, and user viewing activity is kept only locally on-device. - `disabledFunctionalFeatures` - Disables specific functional behaviors (the SDK behaves as if the server has disabled those features). ### `disabledFunctionalFeatures` toggles - `pageReadStatus` - Disables read/unread storage for Story pages; lists/players behave as if read tracking is disabled (no previously viewed indicators). - `clipViewedStatus` - Disables viewed/not viewed storage for Clips; lists/players behave as if viewed tracking is disabled (no previously watched indicators). - `pollVotes` - Poll votes are not persistently stored; users can vote again after navigating away and back. (Coupled with disabled Storyteller analytics, votes won’t contribute to overall Poll statistics.) - `triviaQuizAnswers` - Trivia answers and progress are not persistently stored; users can answer again after navigating away and back. The results page is hidden. - `clipLikes` - Clip like/unlike interactions are not persistently stored; leaving and returning shows the original unliked state. - `clipShares` - Disables Clip Share tracking/storage (but not the sharing action itself); leaving and returning shows the original share count. - `all` - Disables all functional feature behaviors. ## Examples Initialization with custom tracking options: ```Swift // Using custom tracking options let trackingOptions = StorytellerEventTrackingOptions( enablePersonalization: false, enableStorytellerTracking: false, enableUserActivityTracking: false, enableAdTracking: false, enableFullVideoAnalytics: false, enableRemoteViewingStore: false, disabledFunctionalFeatures: [] ) try await Storyteller.shared.initialize( apiKey: "your-api-key", userInput: StorytellerUserInput(externalId: "user-id"), eventTrackingOptions: trackingOptions ) // Or use the default .enableAll (all tracking enabled) try await Storyteller.shared.initialize( apiKey: "your-api-key", userInput: StorytellerUserInput(externalId: "user-id") ) ``` ## Pitfalls / Notes - `eventTrackingOptions` can only be set during initialization. - `Storyteller.shared.eventTrackingOptions` is publicly readable, but cannot be modified at runtime. - Disabling user-activity tracking gates `onUserActivityOccurred()`; it does not remove the separate host-supplied Ad loading callbacks. ## Cross-References - Showcase initialization with `StorytellerEventTrackingOptions`: https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/main/ShowcaseApp/Storyteller/StorytellerService.swift#L37 # Storyteller Delegates URL: /StorytellerDelegate/ ## Task You are helping an engineer integrate Storyteller. Use this documentation to answer questions, propose improvements, and point out missing details. Be precise and call out assumptions. ## Metadata - Slug: `storyteller-delegate` - Source: `public-docs/StorytellerDelegate.md` - Audience: iOS SDK integrators - Platforms: `iOS` - Related: `StorytellerModule.md`, `AnalyticsIntegration.md`, `Analytics.md`, `Ads.md`, `Users.md`, `NavigatingToApp.md`, `Deeplinking.md`, `Themes.md`, `Troubleshooting.md` ## Overview - `StorytellerDelegate` is the global callback interface for Storyteller player events and app-provided integrations (analytics forwarding, ads, in-app navigation, etc). - `StorytellerDelegate` inherits from `StorytellerModule`, so it participates in the same module pipeline (`StorytellerModule.md`). - Every delegate method has a default implementation, including the inherited Ad methods; implement only the callbacks your app needs. - The SDK holds a weak reference to `Storyteller.shared.delegate`; store your delegate strongly in your app. - `log(message:)` receives Storyteller SDK error and informational messages in debug or release builds; failed-request URLs can include a hashed user ID when remote viewing is enabled and custom-attribute values when personalization is enabled, so both must be redacted before forwarding or sharing the logs. - `Storyteller.shared.useCustomShareHandling` lets the host app intercept Story and Clip share taps and receive the SDK-generated `text`, `title`, and `url` via `onShareButtonTapped(text:title:url)` instead of showing the native iOS share sheet; the SDK pauses playback first, and the host should later call `Storyteller.shared.resumePlayer()`. - `StorytellerListViewDelegate` is the UIKit callback route for Story and Clip rows/grids; SwiftUI receives equivalent events through `StorytellerListActionCallback`. - Embedded Clips uses `StorytellerClipsViewControllerDelegate` in UIKit and the `StorytellerClipsView` action closure in SwiftUI. - If you disable automatic player opening via `theme.lists.enablePlayerOpen = false`, handle tile taps through the framework's list callback route (except on SDK-owned screens where the SDK always opens the Player). ## When To Use - You want to observe and forward Storyteller analytics events into your own analytics system. - You want the integrating app to provide fullscreen or bottom banner ads (when configured in the CMS). - You need custom navigation handling for CMS action buttons (`deeplink`) into your app. - You want to replace the SDK-owned iOS share sheet with your own host-managed share experience. - You want lifecycle hooks for when the player is presented/dismissed to pause/resume your own media. - You need to capture Storyteller SDK diagnostics through an app-owned logging destination. - You need custom behavior for list rows/grids (loading state, tap handling, error handling). ## Integration Steps 1. Implement `StorytellerDelegate` and assign it to `Storyteller.shared.delegate` (store it strongly). 2. If you want to own the share UI, set `Storyteller.shared.useCustomShareHandling = true`, implement `onShareButtonTapped(text:title:url)`, and call `Storyteller.shared.resumePlayer()` when your custom share UI is dismissed. 3. (Optional) Implement `StorytellerModule` modules for analytics/ads and register them in the SDK; the delegate runs last. 4. For rows/grids, choose one component callback route: - UIKit: implement `StorytellerListViewDelegate`, assign it to the view's `delegate`, then call `reloadData()`. - SwiftUI: pass an exhaustive `StorytellerListActionCallback` to the row/grid wrapper. 5. If you set `theme.lists.enablePlayerOpen = false`, handle navigation through `onTileTapped(type:)` or `.onTileTapped(type:)` as appropriate. ## API Cheat Sheet ### `StorytellerDelegate` - `onUserActivityOccurred(type:data:)` - Called when the SDK triggers an analytics event; use to forward events to your analytics system. Follow `AnalyticsIntegration.md` for setup and verification, then use `Analytics.md` for event fields. - `getAd(for:) async throws -> StorytellerAd` - Called when the tenant is configured to request fullscreen ads from the integrating app. Return an ad or throw to indicate none is available. - `getBottomBannerAd(for:maxHeight:) async throws -> StorytellerAd` - Called when configured to request bottom banner ads for Clips. `maxHeight` is the maximum allowed banner height for the current layout. - `userNavigatedToApp(url: String)` - Called when a user taps an action button configured as an in-app `deeplink` in the CMS; parse and route within your app. - `onShareButtonTapped(text:title:url)` - Called when `Storyteller.shared.useCustomShareHandling` is `true` and the user taps Share in Stories or Clips. The SDK pauses the current Story or Clip, skips presenting the native iOS share sheet, and forwards the same payload it would otherwise share. Call `Storyteller.shared.resumePlayer()` when your custom share UI is dismissed. - `configureWebView(configuration: inout WKWebViewConfiguration)` - Called before showing a WebView so you can apply custom configuration (iOS only). - `categoryFollowActionTaken(category:isFollowing:)` - Called when a user follows/unfollows a Clips category inside the SDK UI (only in app-managed following mode; see `Users.md#app-managed-following`). - `log(message:)` - Receives SDK error and informational messages in debug and release builds. Redact hashed user IDs, custom-attribute values, and other personal data from request URLs before forwarding or sharing the output; API keys can remain intact when sharing directly with Storyteller support. - `onPlayerPresented()` / `onPlayerDismissed()` - Called when the Story or Clips player is presented or dismissed (useful for pausing/resuming your own media). - `viewController(for category: StorytellerCategory) -> UIViewController?` - Called when the user opens a category screen from Clips (icon tap or interactive swipe). Return a custom view controller or let the SDK present a default. ### `StorytellerListViewDelegate` - UIKit-only callback interface for Story and Clip row/grid views. - `onDataLoadStarted()` - Called when the network request to load list data starts. - `onDataLoadComplete(success:error:dataCount:)` - Called when list data loading completes; use `dataCount` to decide whether to show/hide the list view on error. - `onTileTapped(type: StorytellerTileType)` - Called when a tile is tapped (before the player opens). Use when handling taps yourself or for attribution. - `onPlayerDismissed()` - Called when a Story player opened from a list is dismissed. ### `StorytellerListActionCallback` - SwiftUI callback closure passed to Story and Clip row/grid wrappers. - Receives a `StorytellerListAction` enum value for equivalent `.onDataLoadStarted`, `.onDataLoadComplete(success:error:dataCount:)`, `.onTileTapped(type:)`, and `.onPlayerDismissed` actions. ## Examples Store the global delegate strongly and assign it to Storyteller: ```swift import StorytellerSDK final class AnalyticsDelegate: StorytellerDelegate { func onUserActivityOccurred( type: StorytellerUserActivity.EventType, data: StorytellerUserActivityData ) { print("Storyteller event: \(type), context: \(data.context ?? [:])") } } final class StorytellerIntegration { private let delegate = AnalyticsDelegate() func configure() { Storyteller.shared.delegate = delegate } } ``` Customize Storyteller WebViews with an explicit WebKit import: ```swift import StorytellerSDK import WebKit final class WebViewDelegate: StorytellerDelegate { func configureWebView(configuration: inout WKWebViewConfiguration) { MainActor.assumeIsolated { let script = WKUserScript( source: "document.body.style.backgroundColor = 'red';", injectionTime: .atDocumentEnd, forMainFrameOnly: true ) configuration.userContentController.addUserScript(script) } } } ``` ## Pitfalls / Notes - `Storyteller.shared.delegate` is weak; store your delegate strongly in your app. - Implement `getAd` or `getBottomBannerAd` only when your tenant requests host-supplied Ads; return a real `StorytellerAd` or throw an error. - WebKit types are not re-exported by StorytellerSDK; import `WebKit` in host code that uses `WKWebViewConfiguration`. Storyteller calls this delegate during main-actor UI construction, and `MainActor.assumeIsolated` bridges the released requirement to newer WebKit concurrency annotations. - `onShareButtonTapped(text:title:url)` is only called when `Storyteller.shared.useCustomShareHandling` is `true`; otherwise the SDK keeps the native iOS share sheet flow. - `configureWebView(configuration:)` is available only on iOS. - `categoryFollowActionTaken(category:isFollowing:)` is only called in app-managed following mode. - `onDataLoadStarted()` and `onDataLoadComplete()` are not called until `reloadData()` runs after list delegate assignment. ## Cross-References - Analytics integration setup: `AnalyticsIntegration.md` - Analytics event and payload reference: `Analytics.md` - Showcase delegate: https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/main/ShowcaseApp/Storyteller/StorytellerInstanceDelegate.swift#L11 - Showcase analytics forwarding: https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/main/ShowcaseApp/Analytics/StorytellerTrackingDelegate.swift#L10 # Storyteller Module URL: /StorytellerModule/ ## Task You are helping an engineer integrate Storyteller. Use this documentation to answer questions, propose improvements, and point out missing details. Be precise and call out assumptions. ## Metadata - Slug: `storyteller-module` - Source: `public-docs/StorytellerModule.md` - Audience: iOS SDK integrators - Platforms: `iOS` - Related: `StorytellerDelegate.md`, `Ads.md`, `Analytics.md` ## Overview - `StorytellerModule` is a protocol you can adopt to handle ads fetching and user activity (analytics) callbacks. - `adSource: StorytellerAdSource?` lets modules declare whether ads came from a custom provider, GAM, AdMob, VAST, or Storyteller First Party ads. - Multiple modules can be registered; they are processed in order. - For ads: modules are queried in order; if a module throws, the next module is tried; the delegate is queried last. - For analytics: all modules process `onUserActivityOccurred` in order; the delegate processes it last. - `StorytellerDelegate` also conforms to `StorytellerModule`, so it participates in the same module pipeline. - `StorytellerAdRequestTrackingModule` lets ad modules report the exact unit immediately before each concrete provider attempt. ## When To Use - You want to receive Storyteller analytics events and forward them into your own analytics pipeline. - You want to provide fullscreen or bottom banner ads from your integrating app (when configured in the CMS). - Your host-provided GAM or AdMob module resolves its unit at request time and needs exact `AdRequested` attribution. - You want multiple independent handlers (for example, one module for analytics forwarding and one for ads). ## Integration Steps 1. Adopt `StorytellerModule` in one or more objects. 2. Register modules in the SDK so they are invoked in the desired order (see Showcase registration example). 3. Implement `adSource`, `onUserActivityOccurred`, and/or `getAd` / `getBottomBannerAd` based on your needs. 4. For a host-provided GAM or AdMob module with exact request-time units, set `adSource` to `.gam` or `.admob`, adopt `StorytellerAdRequestTrackingModule`, await its callback immediately before every concrete load, and set the same unit on the returned `StorytellerAd`. ## API Cheat Sheet ### Properties - `adSource: StorytellerAdSource?` - Declares the source for ads provided by your module. - Use `.custom("myNetwork")` for custom integrations, `.gam` for Google Ad Manager, `.admob` for Google AdMob; the SDK-provided VAST module uses `.custom("vast")`; `.storyteller` is reserved for Storyteller First Party ads. - Defaults to `nil` when not implemented; `StorytellerGAMModule`, `StorytellerAdMobModule`, and `StorytellerVASTModule` set it automatically. ### Analytics - `onUserActivityOccurred(type:data:)` - Called when the SDK triggers an analytics event. - `type` is the `StorytellerUserActivity.EventType`; `data` is the corresponding `StorytellerUserActivityData`. - See `Analytics.md` for event types and field definitions. ### Ads - `StorytellerAdAction` supports CTA types: `StorytellerActionType.web`, `StorytellerActionType.inApp`, `StorytellerActionType.externalApp`, and `StorytellerActionType.store`. - `getAd(for:) async throws -> StorytellerAd` - Called when the tenant is configured to request fullscreen ads from the integrating app. - Return a `StorytellerAd` or throw an error to allow the SDK to try the next module. - `getBottomBannerAd(for:maxHeight:) async throws -> StorytellerAd` - Called when configured to request bottom banner ads for Clips. - `maxHeight` is the maximum allowed banner height for the current layout. - `StorytellerAdRequestTrackingModule` - Adds `getAdWithRequestTracking(for:slot:onAdRequested:)` and `getBottomBannerAdWithRequestTracking(for:maxHeight:onAdRequested:)`. - Await `onAdRequested` serially immediately before each native/banner attempt, including fallback attempts, and do not retain it. - Set the reported unit on `StorytellerAd.adUnitId`; honor the optional full-screen `slot` when the provider supports load cancellation. ## Examples Analytics handler: ```swift func onUserActivityOccurred(type: StorytellerUserActivity.EventType, data: StorytellerUserActivityData) { if type == .OpenedStory { // Retrieve the story id value let openedStoryId = data.storyId // Retrieve the story title value let openedStoryTitle = data.storyTitle // Report retrieved values from your app } } ``` Fullscreen ad provider: ```swift func getAd(for adRequestInfo: StorytellerAdRequestInfo) async throws -> StorytellerAd { // Action to get an ad if let ad = await getMyAd() { // Provide the ad to the SDK return ad } else { // Throw an error indicating there is no ad throw YourError() } } ``` ## Cross-References - Showcase module registration: https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/main/ShowcaseApp/ShowcaseApp.swift#L95 # Storyteller List Views URL: /StorytellerListViews/ ## Task You are helping an engineer integrate Storyteller. Use this documentation to answer questions, propose improvements, and point out missing details. Be precise and call out assumptions. ## Metadata - Slug: `storyteller-list-views` - Source: `public-docs/StorytellerListViews.md` - Audience: iOS SDK integrators - Platforms: `iOS` - Related: `Themes.md`, `StorytellerDelegate.md`, `Analytics.md#context`, `StorytellerHome.md` ## Overview - Storyteller list views come in four main variants: Stories/Clips × Row/Grid. - UIKit uses `StorytellerStoriesRowView`, `StorytellerStoriesGridView`, `StorytellerClipsRowView`, and `StorytellerClipsGridView`; SwiftUI uses the matching wrappers with `StorytellerStoriesListModel` or `StorytellerClipsListModel`. - The configuration concepts are shared across frameworks. UIKit applies a configuration with `configure(with:)`; SwiftUI supplies it when creating the model. - Rows are horizontally scrolling lists; grids are vertical lists organized into columns. - Grids can be scrollable (recycling + pull-to-refresh) or non-scrollable (auto-sized, intended for embedding in a larger scroll view). - Non-scrollable grids can hang with very large item counts; use `displayLimit` or a scrollable grid. Without a limit, non-scrollable grids default to rendering at most 30 items. - If you do not set explicit height constraints for rows, the SDK can auto-calculate and adjust row height (including Dynamic Type adjustments). - UIKit requires `reloadData()` after `configure(with:)` to perform the initial fetch; call `reloadData()` on the UIKit view for later refreshes. SwiftUI wrappers perform their initial load from the supplied model; call `reloadData()` on the SwiftUI model for later refreshes. - `StorytellerClipsListConfiguration` configures Clip tiles; use `StorytellerClipCollectionConfiguration` for an embedded or presented Clip Player. - Use `context` in configurations to include attribution metadata in analytics callbacks (`Analytics.md#context`). ## When To Use - You want to embed Stories or Clips lists into your own screens (UIKit or SwiftUI). - You want a row in a feed, or a grid that can be a “More” screen (scrollable grid). - You want to control whether the SDK opens the player automatically on tile taps (via Themes). ## Integration Steps 1. Choose the list view type (Stories vs Clips, Row vs Grid, and for grids: scrollable vs non-scrollable). 2. Create the matching `StorytellerStoriesListConfiguration` or `StorytellerClipsListConfiguration`. 3. Choose one framework route: - UIKit: instantiate and constrain the view, optionally assign `StorytellerListViewDelegate`, call `configure(with:)`, then call `reloadData()` to perform the initial fetch. - SwiftUI: retain a `StorytellerStoriesListModel` or `StorytellerClipsListModel`, pass it and a `StorytellerListActionCallback` to the matching wrapper. 4. For later refreshes, call `reloadData()` on the UIKit view or SwiftUI model. ## API Cheat Sheet ### UIKit list views - `StorytellerStoriesRowView` / `StorytellerClipsRowView` - Horizontal lists. Height can be explicit or auto-calculated by the SDK (supports Dynamic Type adjustments). - `StorytellerStoriesGridView(isScrollable:)` / `StorytellerClipsGridView(isScrollable:)` - Vertical grids. Use `isScrollable: true` for large lists and recycling; `false` for embedding with auto-sized height. ### Configurations - `StorytellerStoriesListConfiguration` - Stories-specific: `categories: [String]` (only on Story views). - `StorytellerClipsListConfiguration` - Clips-specific: `collectionId: String` (only on Clip views). - Common configuration fields (Stories + Clips) - `cellType`: round vs rectangular cells (rectangular aspect ratio 2/3). - `theme`: list + player appearance (see `Themes.md`). - `uiStyle`: `light` / `dark` / `auto` (default `auto` uses system). - `displayLimit`: optional item limit. `nil`, zero, or negative means no limit for rows and scrollable grids; non-scrollable grids default to 30 when no limit is set. - `visibleTiles`: target number of visible row tiles at the default system text size; the row adjusts height and can adapt the count for Dynamic Type (avoid a fixed row height). - `context`: optional dictionary included in analytics callbacks for attribution. ### Delegates - `StorytellerListViewDelegate` (see `StorytellerDelegate.md`) - UIKit callback route for load lifecycle (`onDataLoadStarted`, `onDataLoadComplete`), taps (`onTileTapped`), and Player dismissal (`onPlayerDismissed`). - `StorytellerListActionCallback` - SwiftUI callback route for the equivalent `.onDataLoadStarted`, `.onDataLoadComplete`, `.onTileTapped`, and `.onPlayerDismissed` actions. ### SwiftUI wrappers - Stories: `StorytellerStoriesRow`, `StorytellerStoriesGrid` - Clips: `StorytellerClipsRow`, `StorytellerClipsGrid` - Configure via models (`StorytellerStoriesListModel`, `StorytellerClipsListModel`) which mirror the UIKit configuration options. ## Examples Configure Stories and Clips rows with context: ```swift import StorytellerSDK let storytellerStoriesRow = StorytellerStoriesRowView() let storytellerClipsRow = StorytellerClipsRowView() // Stories configuration with context storytellerStoriesRow.configure(with: StorytellerStoriesListConfiguration( categories: ["sports", "entertainment"], context: [ "source": "home-screen-stories", "campaign": "summer-league" ] )) storytellerStoriesRow.reloadData() // Clips configuration with context storytellerClipsRow.configure(with: StorytellerClipsListConfiguration( collectionId: "trending-clips", context: [ "source": "home-screen-clips", "campaign": "summer-league" ] )) storytellerClipsRow.reloadData() ``` For SwiftUI, construct `StorytellerStoriesListModel` or `StorytellerClipsListModel` with the matching configuration, pass the model and an exhaustive `StorytellerListActionCallback` to the row/grid wrapper, and call the model's mutating `reloadData()` method to refresh. ## Pitfalls / Notes - Non-scrollable grids auto-resize when `onDataLoadComplete` fires but can hang with hundreds of items; set `displayLimit` or switch to a scrollable grid (defaults to 30 items when no limit is provided). - If you omit explicit row heights, the SDK auto-calculates size and applies Dynamic Type adjustments—avoid conflicting manual height constraints. - Do not use `StorytellerClipCollectionConfiguration` for Clip list tiles or `StorytellerClipsListConfiguration` for a Clip Player; the types serve different surfaces. - Interface Builder does not detect the `isScrollable` IBInspectable on third-party views; add a `User Defined Runtime Attribute` named `isScrollable` (Boolean) to toggle scrolling. ## Cross-References - Themes and UI overrides: `Themes.md` - List events and callbacks: `StorytellerDelegate.md` - Showcase Home feed composition: https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/main/ShowcaseApp/Views/Home/HomeView.swift#L162 - Showcase list wrappers: https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/main/ShowcaseApp/Views/Home/Components/StorytellerLists.swift#L11 # Storyteller Home URL: /StorytellerHome/ ## Task You are helping an engineer integrate Storyteller. Use this documentation to answer questions, propose improvements, and point out missing details. Be precise and call out assumptions. ## Metadata - Slug: `storyteller-home` - Source: `public-docs/StorytellerHome.md` - Audience: iOS SDK integrators - Platforms: `iOS` - Related: `Themes.md`, `Analytics.md#context`, `StorytellerListViews.md` ## Overview - `StorytellerHome` embeds multiple Stories/Clips rows and grids into a single list-style screen in your app. - Both framework routes use `StorytellerHomeConfiguration(homeId:theme:uiStyle:context:)`. - `context` is included in analytics callbacks for attribution (`Analytics.md#context`). - SwiftUI uses `StorytellerHomeModel` + `StorytellerHome(model:)`. - UIKit uses `StorytellerHomeView(configuration:)`. - Refresh via pull-to-refresh (iOS 15+) or `reloadData`. ## When To Use - You want an SDK-provided “Home” component that mixes Stories and Clips lists in one embedded view. - You want a configurable feed-like layout driven by a `homeId` configured in Storyteller. ## Integration Steps 1. Create a `StorytellerHomeConfiguration` with your `homeId` and optional `theme`, `uiStyle`, and `context`. 2. Choose one framework route: - UIKit: create `StorytellerHomeView(configuration:)` and add it to a constrained view hierarchy. - SwiftUI: retain `StorytellerHomeModel(configuration:)` and render `StorytellerHome(model:)`. 3. Refresh via `reloadData()`; SwiftUI also provides pull-to-refresh from iOS 15. ## API Cheat Sheet - `StorytellerHomeConfiguration` - `homeId`: identifies the Home configuration to load. - `theme`: optional; if omitted, `Storyteller.shared.theme` is used. - `uiStyle`: `light` / `dark` / `auto` (default `auto` uses system). - `context`: optional dictionary included in analytics callbacks for attribution. - `StorytellerHomeModel(configuration:)` - SwiftUI model used by `StorytellerHome`. - `StorytellerHome(model:)` - SwiftUI view that renders the configured Home (supports pull-to-refresh starting iOS 15). - `StorytellerHomeView(configuration:)` - UIKit view for embedding Home in a `UIViewController`. ## Examples Configuration object: ```swift var theme = StorytellerTheme() // Customize the theme let config = StorytellerHomeConfiguration( homeId: "YOUR_HOME_ID", theme: theme, uiStyle: .auto, context: [ "source": "main-tab", "user_segment": "premium", "variant": "personalized" ] ) ``` SwiftUI: ```swift struct ContentView: View { // Initialize model with configuration @StateObject private var model = StorytellerHomeModel( configuration: StorytellerHomeConfiguration(homeId: "YOUR_HOME_ID") ) var body: some View { StorytellerHome(model: model) } } ``` UIKit: ```swift final class HomeViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let homeView = StorytellerHomeView( configuration: StorytellerHomeConfiguration(homeId: "YOUR_HOME_ID") ) homeView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(homeView) NSLayoutConstraint.activate([ homeView.topAnchor.constraint(equalTo: view.topAnchor), homeView.bottomAnchor.constraint(equalTo: view.bottomAnchor), homeView.leadingAnchor.constraint(equalTo: view.leadingAnchor), homeView.trailingAnchor.constraint(equalTo: view.trailingAnchor) ]) } } ``` ## Cross-References - UIKit sample: https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/cocoapods/StorytellerSampleApp/Components/MultipleLists/MultipleListsViewController.swift#L11 # Embedded Clips URL: /EmbeddedClips/ ## Task You are helping an engineer integrate Storyteller. Use this documentation to answer questions, propose improvements, and point out missing details. Be precise and call out assumptions. ## Metadata - Slug: `embedded-clips` - Source: `public-docs/EmbeddedClips.md` - Audience: iOS SDK integrators - Platforms: `iOS` - Related: `Themes.md`, `Analytics.md` ## Overview - UIKit embeds the Clips Player with `StorytellerClipsViewController`; SwiftUI uses `StorytellerClipsView` with a retained `StorytellerClipsModel`. - Both routes use `StorytellerClipCollectionConfiguration`. UIKit applies it with `configure(with:)`; SwiftUI supplies it when creating the model. - `StorytellerClipCollectionConfiguration` is for an embedded or presented Clip Player; `StorytellerClipsListConfiguration` is for Clip row/grid tiles, and the types are not interchangeable. - Use `reloadData()` to force a reload when configuration values didn’t change. - The SDK auto-pauses/resumes on common visibility changes; use `willHide()` / `willShow()` for custom obscuring scenarios. - `canGoBack` is `true` only when the SDK has an internally pushed category screen to pop; root-level navigation belongs to the host through `topLevelBackTapped` or `.onTopLevelBackTapped`. - `topLevelBackButtonEnabled` defaults to `false` for both UIKit and SwiftUI. ## When To Use - You want a Clips player embedded inside your own navigation structure (for example as a tab). - You want Clips to behave like a “screen” in your app without presenting a modal/fullscreen player. ## Integration Steps 1. Create a shared `StorytellerClipCollectionConfiguration(collectionId: ...)`. 2. Choose one framework route: - UIKit: embed `StorytellerClipsViewController`, call `configure(with:)`, and optionally assign `StorytellerClipsViewControllerDelegate`. - SwiftUI: retain `StorytellerClipsModel(configuration:)`, render `StorytellerClipsView(model:action:)`, and handle actions in its callback. 3. Use `reloadData()` on the view controller or model to force refresh. 4. Use `willHide()` / `willShow()` on the view controller or model for custom auto-pause hints. ## API Cheat Sheet ### UIKit: `StorytellerClipsViewController` - `topLevelBackButtonEnabled` (default: `false`) - Shows the back button when at the top level of the collection. - `canGoBack` - Indicates whether the SDK's internal navigation stack has a previous category screen to pop. - Returns `true` on an internally pushed category screen. - Returns `false` at the collection root or before internal navigation is ready; it does not describe the host app's navigation stack. - `delegate` - Delegate responsible for relaying events from the embedded Clips player. - `configure(with configuration: StorytellerClipCollectionConfiguration)` - Loads (or reuses) Clips for the provided configuration. - Note: does not reload if `configuration.collectionId` or `configuration.destination` did not change; use `reloadData()` to force refresh. - `reloadData()` - Triggers a new request and resets the player to its initial state. - If category filters are applied, navigates back one level instead of reloading the entire collection. - `willHide()` / `willShow()` - Manual visibility hints for auto-pause behavior (for example when presenting a modal over the player). - Auto-pausing respects the user's explicit pause state (if user paused manually, `willShow()` will not resume). ### Configuration: `StorytellerClipCollectionConfiguration` - Use this configuration for `StorytellerClipsViewController`, `StorytellerClipsModel`, or `Storyteller.shared.openCollection(...)`; use `StorytellerClipsListConfiguration` only with Clip list views. - `collectionId` - ID of the collection to display. - `destination` - `clip` or `category` to open initially; defaults to first clip if missing/invalid. - `theme` - Overrides the appearance for this embedded player; global theme applies if not specified. - `openReason` - Analytics action type tracking why the collection was opened; if `nil`, handled internally. - `context` - Optional attribution data included in analytics callbacks. - `adConfiguration` - Optional per-presentation Ad placement controls. - When omitted or set to `nil`, opening pre-roll and bottom banner Ads are not opted in locally. - Pass `StorytellerClipsAdConfiguration` to opt into individual placements when the tenant is remotely configured for the matching Clips Ad placement. ### Delegate: `StorytellerClipsViewControllerDelegate` - `func onDataLoadStarted()` - Called when data loading begins. - `func onDataLoadComplete(success: Bool, error: Error?)` - Called when data loading finishes. - `func topLevelBackTapped()` - Called when back is tapped at top level (if enabled). Default behavior attempts to pop from nearest navigation controller; implement for custom navigation. ### SwiftUI - `StorytellerClipsView(model: StorytellerClipsModel)` - SwiftUI wrapper for embedded Clips. - `StorytellerClipsModel(configuration:topLevelBackButtonEnabled:)` - Holds `configuration` and exposes `canGoBack`, `reloadData()`, `willShow()`, and `willHide()`. - `topLevelBackButtonEnabled` defaults to `false`. - Handle `.onTopLevelBackTapped` from `StorytellerClipsView` when root-level navigation belongs to the SwiftUI host. ### Callback Routes - UIKit: `StorytellerClipsViewControllerDelegate` receives `onDataLoadStarted()`, `onDataLoadComplete(success:error:)`, and `topLevelBackTapped()`. - SwiftUI: the `StorytellerClipsView` action closure receives `.onDataLoadStarted`, `.onDataLoadComplete(success:error:)`, and `.onTopLevelBackTapped`. ## Examples Embed in a `UITabBarController`: ```swift let embeddedClipsVC = StorytellerClipsViewController() let tabBarVC = UITabBarController() tabBarVC.setViewControllers([embeddedClipsVC, someOtherVC], animated: false) ``` Embed via view controller containment: ```swift // somewhere inside a view controller let embeddedClipsVC = StorytellerClipsViewController() addChild(embeddedClipsVC) view.addSubview(embeddedClipsVC.view) // layout the view embeddedClipsVC.didMove(toParent: self) ``` Configure the collection: ```swift let configuration = StorytellerClipCollectionConfiguration(collectionId: "top-plays") embeddedClipsVC.configure(with: configuration) ``` Opt into opening pre-roll Ads for one embedded Clips presentation: ```swift let configuration = StorytellerClipCollectionConfiguration( collectionId: "top-plays", adConfiguration: StorytellerClipsAdConfiguration(preRollEnabled: true) ) embeddedClipsVC.configure(with: configuration) ``` Opt into bottom banner Ads for one embedded Clips presentation: ```swift let configuration = StorytellerClipCollectionConfiguration( collectionId: "top-plays", adConfiguration: StorytellerClipsAdConfiguration(bottomBannerEnabled: true) ) embeddedClipsVC.configure(with: configuration) ``` Destination values: ```swift public enum Destination { case category(id: String) case clip(id: String) } ``` SwiftUI model API: ```swift public var configuration: StorytellerClipCollectionConfiguration public var topLevelBackButtonEnabled: Bool public var canGoBack: Bool public init(configuration: StorytellerClipCollectionConfiguration, topLevelBackButtonEnabled: Bool = false) public func reloadData() public func willShow() public func willHide() ``` SwiftUI embedding: ```swift import SwiftUI import StorytellerSDK struct ExampleSwiftUIView: View { @StateObject var model = StorytellerClipsModel(configuration: StorytellerClipCollectionConfiguration(collectionId: "test-collection")) var body: some View { ZStack { StorytellerClipsView(model: model) } } } ``` ## Pitfalls / Notes - Constrain the view correctly and respect safe area to avoid UI elements being obscured (status/nav/tab bars). - `configure(with:)` is not a forced reload if `collectionId` / `destination` are unchanged. - SDK versions 10.6.0 through 11.5.1 returned the inverse `canGoBack` result. Remove any manual negation workaround when upgrading to 11.6.0 or newer. - Do not pass `StorytellerClipsListConfiguration` to the embedded Player; list configuration controls tiles, while collection configuration controls Player destination, presentation, analytics context, and Ad placements. ## Cross-References - SwiftUI integration example (Showcase): https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/main/ShowcaseApp/Views/Clips/ClipsView.swift#L7 - SwiftUI view usage (Showcase): https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/main/ShowcaseApp/Views/Clips/ClipsView.swift#L57 - UIKit integration (CocoaPods sample): https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/cocoapods/StorytellerSampleApp/Components/EmbeddedClips/EmbeddedClipsViewController.swift#L8 # Storyteller Cards URL: /Cards/ ## Task You are helping an engineer integrate Storyteller. Use this documentation to answer questions, propose improvements, and point out missing details. Be precise and call out assumptions. ## Metadata - Slug: `cards` - Source: `public-docs/Cards.md` - Audience: iOS SDK integrators - Platforms: `iOS` - Related: `Analytics.md`, `Themes.md` ## Overview - Storyteller Cards are themeable components that can show an image or video background with optional title/subtitle/button. - Supported card aspect ratios are `1:1`, `2:3`, `3:4`, `4:5`, `9:16`, `16:9`, and `4:1`. - Video Cards can expose a mute/unmute control based on tenant settings; only the active audio-capable video Card can be audible. - Tapping a Card triggers an action configured in the CMS (for example opening a Story, Category, Clip, Clip Collection, or other CMS-defined actions). - Card collections can be personalized server-side; you configure which collection to display by `collectionId`. - Optional `context` is included in analytics events for attribution (see `Analytics.md#context`). ## When To Use - You want to place a single “hero” or “promo” component that opens Storyteller content or a CMS-configured action. - You want a server-driven, themeable CTA that can be personalized per user. ## Integration Steps ### SwiftUI 1. Create a `StorytellerCardConfiguration` (required: `collectionId`; optional: `context`). 2. Create a `StorytellerCardModel(configuration:)` and store it as an `@StateObject`. 3. Render `StorytellerCard(model:)`. 4. Optionally handle actions like `onDataLoadComplete` to hide the card on failure, show placeholders, etc. ### UIKit 1. Create a `StorytellerCardConfiguration` (required: `collectionId`; optional: `context`). 2. Create `StorytellerCardView(configuration:)` and add it to your view hierarchy. 3. Optionally set `StorytellerCardView.delegate` (conforming to `StorytellerCardViewDelegate`) to handle callbacks like `onDataLoadComplete`. ## API Cheat Sheet ### SwiftUI - `StorytellerCardConfiguration(collectionId:context:)` - Points the component at a CMS card collection; optional `context` is forwarded to analytics for attribution. - `StorytellerCardModel(configuration:)` - `ObservableObject` backing the SwiftUI card view. - `StorytellerCard(model:action:)` - SwiftUI view that renders the Card collection. - Action callback can be used for events like `onDataLoadComplete`. - `StorytellerCardModel.reload()` - Manually refreshes Card data from the server. ### UIKit - `StorytellerCardView(configuration:)` - `UIView` subclass that renders the Card collection. - `StorytellerCardViewDelegate` - Receives callbacks (for example data load completion). - `StorytellerCardView.reload()` - Manually refreshes Card data from the server. - `StorytellerCardCollectionViewCell` / `StorytellerCardTableViewCell` - Prebuilt cells for embedding Cards in collection/table views (follow the same configuration pattern). ### Video Card Audio - `theme.behavior.cards.showMuteToggle` - `true` shows the audio toggle for active audio-capable video Cards; `false`, `nil`, or missing keeps Cards muted with no icon. - `theme.behavior.cards.persistMuteState` - `true` persists the user's Cards mute choice; `false`, `nil`, or missing keeps it scoped to the current session. - `theme.behavior.cards.defaultMuteState` - Supports `soundOff`, `soundOn`, or `respectDeviceSilentToggle`; missing, `nil`, or unrecognized values default to `soundOff`. ## Examples SwiftUI: ```swift import SwiftUI import StorytellerSDK struct SwiftUIView: View { @StateObject private var cardModel = StorytellerCardModel( configuration: StorytellerCardConfiguration( collectionId: "card-collection-id", context: ["source": "hero-banner"] ) ) var body: some View { VStack { Text("Storyteller Card Section") StorytellerCard(model: cardModel) { action in switch action { case .onDataLoadComplete(let result): switch result { case .success: print("Card data loaded successfully!") case .failure(let error): print("Card data failed to load: \(error.localizedDescription)") } } } Button("Reload Card") { cardModel.reload() } } .padding() } } ``` UIKit: ```swift class CardView: UIView, StorytellerCardViewDelegate { private var storytellerCardView: StorytellerCardView? // ... func configure(with collectionId: String, delegate: StorytellerCardViewDelegate?) { let configuration = StorytellerCardConfiguration( collectionId: collectionId, context: [ "source": "hero-banner", ] ) let cardView = StorytellerCardView(configuration: configuration) cardView.delegate = delegate addSubview(cardView) // Add constraints self.storytellerCardView = cardView } func reloadCard() { storytellerCardView?.reload() } } ``` ## Pitfalls / Notes ### Reloading - Both `StorytellerCardModel` (SwiftUI) and UIKit flavours provide `reload()`, which refreshes Card data from the server. ### Viewed/Tapped Ordering - Card collections can be ordered in the CMS based on viewed/tapped status so users see “fresh” content. ### Video Card Audio - Cards share audio state across the current Cards view; if a user unmutes one active video Card, later active video Cards stay unmuted until the user mutes again or an audio interruption occurs. - Only the active audio-capable video Card can be audible; inactive video Cards stay muted. - Image Cards and video Cards marked as having no audio do not show the audio control. ### Theming (CMS) Card appearance and behavior are primarily configured in the Storyteller CMS per Card Collection. The following properties can be configured in the CMS and influence the Card's presentation and behaviour: #### Button Behavior - Button positioning follows `textOverContent`: - When `textOverContent = true`: Button appears on the card (overlaying the content), positioned below the title/subtitle - When `textOverContent = false`: Button appears below the card (below the title/subtitle section) - Buttons do not change card tappability (the entire card remains tappable and executes the same action). - Button text comes from the Card data, not the theme. - `style.textLengthMode` (default: `truncate`): How text that exceeds the available space is handled. - `truncate`: Display text at the specified size; truncate with an ellipsis (...) if it doesn't fit. - `resize`: Start at the specified text size and reduce the font size until the text fits (up to two lines for heading and subheading). - `style.textAlignment` (default: `start`): Horizontal alignment of the heading and subheading. Can be `start`, `center`, or `end`. - `style.padding` (default: `12`): Inner padding around the text content. For full-bleed cards (`marginHorizontal = 0`) 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 `marginHorizontal > 0`, padding is applied only to the top and bottom of the text. - `style.marginHorizontal` (default: `0`): Horizontal margin around the card. `0` means full-bleed. - `style.cornerRadius` (default: `{theme.primitives.cornerRadius}`): Corner radius of the card. The application depends on `marginHorizontal` and text position. Not applied for full-bleed cards (`marginHorizontal=0`) with text *below* the image. Applied to the *image* for cards with text *below* the image and `marginHorizontal > 0`. Applied to the *whole card* for cards with text *on* the image and `marginHorizontal > 0`. - `style.heading.font` (default: `{theme.customFont}`): Font family for the heading. - `style.heading.textSize` (default: `22`): Font size for the heading. - `style.heading.lineHeight` (default: `28`): Line height for the heading. - `style.heading.textCase` (default: `default`): Text case transformation (`upper`, `lower`, `default`). - `style.heading.letterSpacing` (default: `0`): Letter spacing for the heading. - `style.heading.textColor` (default: `{theme.colors.white.primary}`): Text color for the heading. The default applies when text is displayed *on* the background asset. When text is displayed *below* the background, the default color is `{theme.colors.black.primary}` in light mode and `{theme.colors.white.primary}` in dark mode. - `style.subHeading.font` (default: `{theme.customFont}`): Font family for the subheading. - `style.subHeading.textSize` (default: `16`): Font size for the subheading. - `style.subHeading.lineHeight` (default: `21`): Line height for the subheading. - `style.subHeading.textCase` (default: `default`): Text case transformation (`upper`, `lower`, `default`). - `style.subHeading.letterSpacing` (default: `0`): Letter spacing for the subheading. - `style.subHeading.textColor` (default: `{theme.colors.white.secondary}`): Text color for the subheading. The default applies when text is displayed *on* the background asset. When text is displayed *below* the background, the default color is `{theme.colors.black.secondary}` in light mode and `{theme.colors.white.secondary}` in dark mode. #### Button Theme Properties - `style.button.title.font` (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. - `style.button.title.textSize` (default: `16`): Font size for the button text. - `style.button.title.lineHeight` (default: `21`): Line height for the button text. - `style.button.title.textCase` (default: `default`): Text case transformation for the button text (`upper`, `lower`, `default`). - `style.button.title.letterSpacing` (default: `0`): Letter spacing for the button text. - `style.button.title.textColor` (default: `{theme.colors.white.primary}` for text on image, `{theme.colors.black.primary}` for text below image in light mode, `{theme.colors.white.primary}` for text below image in dark mode): Text color for the button. If not specified or null, uses the same color logic as the outline color. - `style.button.backgroundColor` (optional): Background color of the button. If not set, the button will have a transparent background with an outline. - `style.button.outlineColor` (default: `{theme.colors.white.primary}` for text on image, `{theme.colors.black.primary}` for text below image in light mode, `{theme.colors.white.primary}` for text below image in dark mode): Color of the button outline/border. - `style.button.outlineWidth` (default: `1`): Width of the button outline/border in points. - `style.button.cornerRadius` (optional, default: `{theme.primitives.cornerRadius}`): Corner radius of the button. If null or not set, falls back to the theme's default corner radius. - `style.button.textAlignment` (default: uses card `textAlignment`): Text alignment for the button text. If not specified or null, uses the card's text alignment setting. #### Behavior Properties - `behavior.reloading.reloadOnExit` (default: `true`): Whether the Card Collection reloads after returning from tapping a Card (for example after dismissing the Story/Clip player). - `behavior.reloading.reloadOnForeground` (default: `true`): Whether the Card Collection reloads when the app comes to the foreground. ## Cross-References - Showcase SwiftUI usage: https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/main/ShowcaseApp/Views/Home/Components/StorytellerItemView.swift#L10 - Showcase SwiftUI callback handling: https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/main/ShowcaseApp/Views/Home/Components/StorytellerItemView.swift#L95 - UIKit table patterns (CocoaPods sample): https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/cocoapods/StorytellerSampleApp/Components/MultipleLists/TableView/MultipleListsDataSource.swift#L17 # Custom Themes URL: /Themes/ ## Task You are helping an engineer integrate Storyteller. Use this documentation to answer questions, propose improvements, and point out missing details. Be precise and call out assumptions. ## Metadata - Slug: `themes` - Source: `public-docs/Themes.md` - Audience: iOS SDK integrators - Platforms: `iOS` - Related: `StorytellerListViews.md`, `StorytellerHome.md`, `Search.md`, `StorytellerDelegate.md`, `Cards.md` ## Overview - Customize the SDK appearance by assigning a `StorytellerTheme` to `Storyteller.shared.theme`. - You can override the theme per list view by passing a theme in the list configuration. - `StorytellerTheme` contains `light` and `dark` `Theme` values; which one is used depends on the list view’s `uiStyle`. - Theme configuration covers colors, fonts, primitives, list styling, player styling, Cards audio icons, buttons, instructions, engagement units, Search, and Storyteller Home; Followable Category profile styling is provided through remote Settings rather than the public `Theme` API. - Missing Followable Category Profile tab colours use `#FF1A1A1A` / `#991A1A1A` in Light appearance and retain `#FFFFFFFF` / `#D9FFFFFF` in Dark appearance; valid remote fields override their fallback independently. - `theme.player.clips` includes button-background, icon-size, feed-switcher/title/category-navigation typography, readability gradients, spacing, and progress-bar position controls for the Clips Player layout. - `theme.player.clips.progressBar.position` accepts `bottom` or `aboveAction`; `aboveAction` moves the progress bar only for Embedded Clips with a visible primary action, while other layouts retain the bottom position. - `theme.behavior.player.clips.modalContentBottomAnchor` remotely selects video- or screen-anchored lower UI for non-embedded modal Clips Players; `video` is the default. - `theme.search` provides optional, independently resolved light/dark styling for the Search field, suggestions, no-results state, filter affordance, and filter sheet while retaining existing defaults. - Some properties inherit defaults from other theme properties; changing a “base” value can affect multiple UI elements. - `theme.search.filters.applyButton` styles the Search filter action independently from shared `buttons`; omitted colors keep the contrasting black/white light-mode and white/black dark-mode treatment. - `theme.lists.enablePlayerOpen` controls whether list tile taps open the player automatically; when disabled, handle tile taps via `StorytellerListViewDelegate.onTileTapped(type:)` (except on SDK-owned screens where the SDK always opens the player). ## When To Use - You want Storyteller UI to match your app branding (colors, fonts, icons). - You want different styling for specific embedded lists vs global defaults. - You need to change behavior/affordances controlled by theme flags (for example, list auto-open behavior). ## Integration Steps 1. Create a `StorytellerTheme` and customize the desired properties. 2. Set `Storyteller.shared.theme = theme` early in app startup (for example, in `AppDelegate`). 3. (Optional) Pass a theme via list configuration to override styling for a specific list view. ## API Cheat Sheet - `Storyteller.shared.theme` - Global theme applied across SDK components. - `StorytellerTheme` - Has `light` and `dark` themes; the active side is chosen based on list `uiStyle`. - Custom fonts - Subclass the open `StorytellerFontProvider` class, override `font(weight:size:)`, and assign the provider to `theme.light.customFont` and `theme.dark.customFont`. - Gradients - `Theme.Gradient` requires start/end colors and nested `Theme.Gradient.GradientPosition` values; positions are enum cases, not integers. - Per-list override - Provide a `theme` in `StorytellerStoriesListConfiguration` / `StorytellerClipsListConfiguration` (see `StorytellerListViews.md`). - High-impact theme areas (details in the reference below) - `colors`: base palette used throughout the SDK UI. - `customFont`: per-light/dark custom font via an open `StorytellerFontProvider` subclass. - `lists`: list behavior/styling including `enablePlayerOpen` and row/grid/tile settings. - `player`: story/clips player controls (icons, Clips typography/spacing, share/like visibility, live chip styling). - `player.clips.showButtonBackgrounds` / `player.clips.actionIconSize`: controls Clips Player action button backgrounds and icon sizing; `actionIconSize` values outside `24...38` are clamped. - `player.clips.feedSwitcher` / `player.clips.title` / `player.clips.categoryNavigation`: independently resolved Clips typography for tab selection, title, and category-navigation labels. Feed-switcher and category-navigation labels inherit `customFont`; the title keeps its own font override and adds `fontWeight`. - `player.clips.eyebrow`: typography overrides for the Clips Player eyebrow; `lineHeight` defaults to `nil`. - `player.clips.topGradient` / `player.clips.bottomGradient`: independently resolved readability scrims with existing visual defaults when omitted. - `player.clips.progressBar.position`: appearance-scoped `bottom` or `aboveAction` placement. Feed-specific remote, tenant/global remote, and host values resolve in that order before the `bottom` default. - `player.clips.spacing`: layout controls for the Clips Player: `backButtonStartInset`, `contentInsetHorizontal`, `contentInsetBottom`, `actionSpacing`, `eyebrowToTitleSpacing`, `titleToActionSpacing`. - Modal Clips layout: `theme.behavior.player.clips.modalContentBottomAnchor` accepts `video` or `screen`; missing, invalid, or unknown values use `video`, and Embedded Clips are unaffected. - Followable Category profile: `theme.behavior.player.clips.enableProfileScreen` gates the new screen; `theme.light.profileScreen` / `theme.dark.profileScreen` control content availability and styling; missing or invalid fields fall back independently, with appearance-specific Latest/Popular tab colours; Latest/Popular tabs honor `theme.behavior.following.feedSwitcher.selectionStyle`; grid and tile styling still use `lists` and `tiles`. - `cards.audio`: custom muted/unmuted 48x48 Cards audio control visuals for video Cards. - `buttons`: shared button styling. - `instructions`: first-time instructions screen styling/visibility. - `engagementUnits`: Poll/Quiz styling. - `search`: optional Search background, input, filter button, suggestions, no-results, and filter-sheet styling; Search typography inherits `customFont`, result headings use `lists.title`, and Apply Filters uses `search.filters.applyButton` independently from shared `buttons` (see `Search.md`). - `home`: Storyteller Home styling (see `StorytellerHome.md`). ## Examples Global theme: ```swift let myTheme = StorytellerTheme() Storyteller.shared.theme = myTheme ``` Per-list override: ```swift let myRowTheme = StorytellerTheme() storytellerStoriesRow.configure(with: StorytellerStoriesListConfiguration( categories: ["category-id"], theme: myRowTheme )) ``` Custom font provider: ```swift import StorytellerSDK import UIKit final class CustomFontProvider: StorytellerFontProvider, @unchecked Sendable { override func font(weight: StorytellerFontWeight, size: CGFloat) -> UIFont? { switch weight { case .light, .regular, .medium: return UIFont(name: "MyCustomFont", size: size) default: return UIFont(name: "MyCustomFontBold", size: size) } } } var theme = StorytellerTheme() let customFont = CustomFontProvider() theme.light.customFont = customFont theme.dark.customFont = customFont Storyteller.shared.theme = theme ``` Gradient construction: ```swift import StorytellerSDK import UIKit let brandGradient = Theme.Gradient( startColor: UIColor.systemBlue, endColor: UIColor.systemPurple, startPosition: .topCenter, endPosition: .bottomCenter ) ``` ## Cross-References - Showcase theme manager: https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/main/ShowcaseApp/Storyteller/StorytellerThemeManager.swift#L41 # Integrate Analytics URL: /AnalyticsIntegration/ ## Task You are helping an iOS engineer connect Storyteller user activity callbacks to the host app's analytics system. Keep delegate lifetime, initialization-time tracking gates, event payload selection, and per-surface attribution context distinct. ## Metadata - Slug: `analytics-integration` - Source: `public-docs/AnalyticsIntegration.md` - Audience: iOS SDK integrators - Platforms: `iOS` - Related: `Analytics.md`, `PrivacyAndTracking.md`, `StorytellerDelegate.md`, `Troubleshooting.md` ## Overview - Storyteller delivers supported user activity events through `StorytellerDelegate.onUserActivityOccurred(type:data:)`. - `Storyteller.shared.delegate` is weak, so the integrating app must retain its delegate strongly and avoid replacing it accidentally. - Forward the serialized `StorytellerUserActivity.EventType.rawValue` and the event-specific fields needed by the host analytics contract. - `enableUserActivityTracking` gates host user activity callbacks; Ad events additionally require `enableAdTracking`. - `enableStorytellerTracking` controls Storyteller's own collection and is separate from the host callback gate. - Disabling `enableFullVideoAnalytics` preserves callbacks but removes documented content identifiers and titles from the payload. - `StorytellerAnalyticsContext` is `[String: String]` supplied per supported surface or presentation and returned as optional `StorytellerUserActivityData.context` when the event can be attributed to it. - Embedded Clips supports context through `StorytellerClipCollectionConfiguration` in both UIKit and SwiftUI integrations. - Initialization success does not generate a host user activity callback; verify the path with a supported content interaction. ## Integration Steps 1. Create one app-owned `StorytellerDelegate` and keep it strongly retained. 2. Implement `onUserActivityOccurred(type:data:)` and forward the serialized event key plus the payload fields required by the host analytics layer. 3. Assign the delegate to `Storyteller.shared.delegate`. 4. Choose `StorytellerEventTrackingOptions` from the app's consent policy and pass them during SDK initialization. 5. Add `StorytellerAnalyticsContext` to each surface or presentation that needs attribution. 6. Trigger a documented content interaction and verify the event key, selected payload, and attributable context at the host analytics boundary. ## API Cheat Sheet - `Storyteller.shared.delegate` - Weak global delegate property; retain the assigned object in app-owned state. - `StorytellerDelegate.onUserActivityOccurred(type:data:)` - Receives a `StorytellerUserActivity.EventType` and event-specific `StorytellerUserActivityData`. - `StorytellerUserActivity.EventType.rawValue` - Serialized event key to forward into a host analytics contract. - `StorytellerEventTrackingOptions` - Selected during initialization; reinitialize to apply a later consent change. - `StorytellerAnalyticsContext` - Type alias for `[String: String]`; the SDK does not prescribe keys. - `StorytellerUserActivityData.context` - Optional context attributed from the originating supported configuration. ## Context Sources - `StorytellerStoriesListConfiguration` - `StorytellerClipsListConfiguration` - `StorytellerClipCollectionConfiguration` (including Embedded Clips) - `StorytellerCardConfiguration` - `StorytellerHomeConfiguration` ## Diagnostics - No events: verify strong delegate retention, current delegate assignment, and `enableUserActivityTracking`. - Missing Ad events: also verify `enableAdTracking` and that an eligible Ad lifecycle point occurred. - Callback arrives without IDs or titles: verify `enableFullVideoAnalytics`. - Callback arrives without context: verify the active configuration supplied context before content loaded/opened and that the event is attributable to that surface. - Initialization succeeded but no callback arrived: trigger a supported content interaction; initialization does not generate this host callback. ## Cross-References - Full event and payload catalogue: `Analytics.md` - Consent and tracking behavior: `PrivacyAndTracking.md` - Global and component callback ownership: `StorytellerDelegate.md` - Missing callback route: `Troubleshooting.md#callbacks-or-analytics-events-do-not-arrive` - Provider-specific Showcase forwarding: https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/main/ShowcaseApp/Analytics/StorytellerTrackingDelegate.swift#L10 # Ads URL: /Ads/ ## Task You are helping an engineer integrate Storyteller. Use this documentation to answer questions, propose improvements, and point out missing details. Be precise and call out assumptions. ## Metadata - Slug: `ads` - Source: `public-docs/Ads.md` - Audience: iOS SDK integrators - Platforms: `iOS` - Related: `Analytics.md`, `PrivacyAndTracking.md`, `StorytellerDelegate.md`, `AdditionalMethods.md` ## Overview - Storyteller supports tenant-configured first-party ads (CMS-managed), Google Ad Manager (GAM) ads via `StorytellerGAMModule`, Google AdMob ads via `StorytellerAdMobModule`, generic VAST tags via `StorytellerVASTModule`, GAM VAST tags via `StorytellerGAMVASTModule`, and custom ads implemented by the integrator. - `StorytellerGAMModule` and `StorytellerAdMobModule` are mutually exclusive; if first-party ads are enabled for your tenant, no integration changes are needed. - GAM and AdMob use the `StorytellerGAMIntegration` package with `StorytellerGAMModuleConfiguration` or `StorytellerAdMobModuleConfiguration`; `bannerAdUnit` handles fullscreen banner fallback/priority and `bottomBannerAdUnit` handles Clips bottom banners. - VAST uses `StorytellerVASTModuleConfiguration`; GAM VAST uses `StorytellerGAMVASTModule` in the same `StorytellerVASTIntegration` package to build GAM VAST tag requests without Google Mobile Ads or IMA. - See the "VAST and GAM VAST Parameter Tables" section for SDK-owned parameters, host-provided parameters, and override behavior. - VAST supports fullscreen Story and Clip ads with compatible linear VAST 2.x / 3.x / 4.x parsing, wrappers, media selection, click-through/click tracking, playback tracking, skip tracking, VAST error reporting, branded metadata, and static-resource icon overlays. - VAST v1 does not support nonlinear ads, companion ads, OMID / verification rendering, VPAID / SIMID, SSAI, Clips bottom banner ads, VAST ad caption rendering, or IFrame/HTML icon rendering. - Default GAM key-value pairs (KVPs) are only sent when ad tracking is enabled (`enableAdTracking == true` in the `StorytellerEventTrackingOptions` passed to `Storyteller.shared.initialize(...)`). - GAM `configureAdRequest` is an async main-actor callback called after Storyteller applies request setup, default KVPs, `customKvps`, and `publisherProvidedId`, but before the Google request is loaded; Storyteller awaits it before calling the Google load API. - For Clips bottom banner ads with GAM, supply `bottomBannerAdUnit` in `StorytellerGAMModuleConfiguration` and opt the presentation in with `StorytellerClipsAdConfiguration(bottomBannerEnabled: true)`. - The tenant feed response must also expose the Clips bottom-banner placement with `showBottomBannerAd == true`. Storyteller First Party Ads, `StorytellerVASTModule`, and `StorytellerGAMVASTModule` do not support it. - Opening fullscreen pre-roll is disabled by default for Clips presentations. To opt in, pass `StorytellerClipsAdConfiguration(preRollEnabled: true)` through `StorytellerClipCollectionConfiguration.adConfiguration`; the tenant must still be configured with Clips ads using `initialIndex = 0`. The request uses the first opened Clip as context, sends `adIndex = 1` / `stAdIndex = 1`, and starts content if the CMS-configured opening timeout is reached first. - For custom ads, set `StorytellerModule.adSource` to declare the provider and use `StorytellerAdRequestInfo` for request context; the GAM, AdMob, VAST, and GAM VAST modules set `adSource` automatically. ## When To Use - You want Storyteller to render ads in Stories and/or Clips. - You’re using Google Ad Manager and need Storyteller to request ads via the GAM integration. - You’re using Google AdMob and need Storyteller to request native Ads, fullscreen banner fallback Ads, and/or Clips bottom banner Ads. - You’re using a compatible HTTPS VAST tag and need Storyteller to request, parse, resolve, and render fullscreen VAST video ads. - You’re using Google Ad Manager to serve VAST video tags and want Storyteller to build the GAM VAST tag request without adding Google Mobile Ads or IMA. - You’re implementing your own ad fetching/rendering and need request context (`StorytellerAdRequestInfo`). ## Integration Steps 1. Confirm with Storyteller which ad source is enabled for your tenant (first-party, GAM, AdMob, VAST, or custom). 2. For GAM or AdMob: install the `StorytellerGAMIntegration` extension (SPM or CocoaPods). 3. Create a `StorytellerGAMModuleConfiguration`: - Required: `adUnit` closure that returns an Ad Unit ID per request context. - Optional: `bottomBannerAdUnit` for Clips bottom banner placement. - Optional: `customNativeTemplateIds`, `customKvps` if you’ve coordinated those with Storyteller. - Optional: `publisherProvidedId` closure that returns a Publisher Provided ID to include in GAM ad requests. - Optional: `configureAdRequest` async main-actor closure for host-owned Google request mutation before load. 4. Create a `StorytellerAdMobModuleConfiguration`: - Required: `adUnit` closure that returns the native Ad Unit ID. - Optional: `bannerAdUnit` for fullscreen banner fallback or banner-first loading; use a separate Ad Unit from `adUnit`. - Optional: `bottomBannerAdUnit` for Clips bottom banner placement. - Optional: `customKvps`, `enableBannerAdPriority`. 5. For generic VAST: install `StorytellerVASTIntegration`, then create a `StorytellerVASTModuleConfiguration` with an HTTPS `baseUrl`, per-request parameters, and a URL format. 6. For GAM VAST: install `StorytellerVASTIntegration`, then create a `StorytellerGAMVASTModuleConfiguration` with `adUnit`, `descriptionUrl`, optional `contentUrl`, optional `customParams`, and optional `tagParameters`. 7. For custom ads: implement `StorytellerModule` or `StorytellerDelegate`, set `adSource`, and use `StorytellerAdRequestInfo`. 8. Register only the selected ads module via `Storyteller.shared.modules` (see examples). ## API Cheat Sheet ### GAM module - SPM: `https://github.com/getstoryteller/storyteller-gam-module-swift` - Install the Storyteller GAM integration as a Swift package. - CocoaPods: `pod 'StorytellerGAMIntegration'` - Install the integration via CocoaPods (make sure your sources include the Storyteller podspec repo). - CocoaPods sources: `source 'https://github.com/getstoryteller/storyteller-sdk-ios-podspec.git'`, `source 'https://github.com/getstoryteller/storyteller-lottie-ios-podspec.git'`, and `source 'https://cdn.cocoapods.org/'` - `StorytellerGAMModuleConfiguration(adUnit: ...)` (required) - Provide an `adUnit` closure that maps a request context (Stories vs Clips) to a GAM Ad Unit ID. - `StorytellerGAMModuleConfiguration.bottomBannerAdUnit` (optional; Clips bottom banner) - Provide a separate Ad Unit ID specifically for the Clips bottom banner placement. - `StorytellerGAMModuleConfiguration.customNativeTemplateIds` (optional) - Provide custom native template IDs if you have those configured with the Storyteller Delivery team. - `StorytellerGAMModuleConfiguration.publisherProvidedId` (optional) - Provide a Publisher Provided ID (PPID) for Google Ad Manager audience targeting. - `StorytellerGAMModuleConfiguration.customKvps` (optional) - Provide extra KVPs per ad request; note the SDK does not inherit any KVPs from the rest of your app. Do not pass PPID here; use `publisherProvidedId` for PPID. - `StorytellerGAMModuleConfiguration.configureAdRequest` (optional) - Await host bidder work and mutate the prepared Google Mobile Ads request after Storyteller targeting and PPID are applied, but before the request is loaded. - `Storyteller.shared.modules = [StorytellerGAMModule(configuration: configuration)]` - Register the module so Storyteller can request/render ads via GAM. ### VAST module - SPM: `https://github.com/getstoryteller/storyteller-vast-module-swift` - Install the Storyteller VAST integration as a Swift package. - CocoaPods: `pod 'StorytellerVASTIntegration'` - Install the integration via CocoaPods (make sure your sources include the Storyteller podspec repo). - `StorytellerVASTModuleConfiguration(baseUrl:requestParameters:urlFormat:diagnosticsHandler:)` - Provide an HTTPS VAST endpoint, a closure that returns VAST tag parameters for each `StorytellerAdRequestInfo`, an optional `.pathSegment` or `.queryString` URL format, and optional diagnostics. - `.pathSegment` - Appends VAST parameters as `/key=value` path segments. - `.queryString` - Appends VAST parameters as regular URL query parameters. - `Storyteller.shared.modules = [StorytellerVASTModule(configuration: configuration)]` - Register the module so Storyteller can request/render fullscreen VAST ads. - VAST supports fullscreen Story and Clip ads; it does not serve Clips bottom banner ads. - VAST tracking: - Multiple URLs for the same supported VAST event are preserved and fired for the matching Storyteller playback event. - VAST `skip` tracking maps to the Storyteller skipped-ad flow. - VAST `skipoffset` becomes an ad-specific non-skippable countdown duration; when absent, the SDK uses the tenant's CMS-configured non-skippable ads behavior. - VAST branded presentation metadata: - Use `Extension type="storyteller:ad-ui"` with `StorytellerAdUi/CtaText` and `AdvertiserName`. - CTA text is shown only when the VAST ad has a non-empty ``. - VAST icon overlays: - Story and Clip VAST ads render one compatible static-resource `` overlay inside the actual video frame only when `program="AdChoices"`. - `xPosition` supports `left`, `right`, or a numeric x-coordinate; `yPosition` supports `top`, `bottom`, or a numeric y-coordinate. - The overlay height is fixed at 24pt; width preserves the static resource aspect ratio. - Non-AdChoices, ``, and `` icons are parsed but not rendered. - VAST `offset`, `duration`, `IconViewTracking`, `IconClickTracking`, and `IconClickThrough` are honored. ### GAM VAST module - SPM: `https://github.com/getstoryteller/storyteller-vast-module-swift` - Install the Storyteller VAST integration as a Swift package. - CocoaPods: `pod 'StorytellerVASTIntegration'` - Install the VAST integration via CocoaPods. - `StorytellerGAMVASTModuleConfiguration(adUnit:descriptionUrl:contentUrl:customParams:tagParameters:diagnosticsHandler:)` - `adUnit` is required and becomes the GAM `iu` parameter. - `descriptionUrl` is required and becomes the GAM `description_url` parameter. - `contentUrl` is optional and becomes the GAM `url` parameter when non-`nil`. - `customParams` is optional custom targeting; the SDK serializes it into GAM `cust_params`, so pass unencoded keys and values. - `tagParameters` is optional extra top-level GAM VAST tag parameters; matching keys override SDK-generated values. - `diagnosticsHandler` receives the same VAST diagnostics as `StorytellerVASTModule`. - `Storyteller.shared.modules = [StorytellerGAMVASTModule(configuration: configuration)]` - Register the module so Storyteller can request/render fullscreen GAM VAST ads. - `StorytellerGAMVASTModule` requests `https://pubads.g.doubleclick.net/gampad/ads` with query-string parameters and supplies `iu`, `output=vast`, `env=vp`, `gdfp_req=1`, `sz`, `correlator`, `description_url`, optional `url`, `vpa=auto`, `vpmute`, and optional `cust_params`. - GAM VAST is VAST-backed, not Google Mobile Ads or IMA-backed; it sets `adSource` to `.custom("vast")` and does not emit Google paid ad analytics events. - Use the "VAST and GAM VAST Parameter Tables" section to check SDK-owned parameters, `customParams` encoding into `cust_params`, and `tagParameters` override behavior. - GAM VAST supports fullscreen Story and Clip ads; it does not serve Clips bottom banner ads. ### AdMob module - SPM: `https://github.com/getstoryteller/storyteller-gam-module-swift` - Install the same Storyteller integration package used for the GAM module. - CocoaPods: `pod 'StorytellerGAMIntegration'` - Install the same integration via CocoaPods (make sure your sources include the Storyteller podspec repo). - `StorytellerAdMobModuleConfiguration(adUnit:bannerAdUnit:bottomBannerAdUnit:customKvps:enableBannerAdPriority:)` - `adUnit` is required and returns the native Ad unit ID. - `bannerAdUnit` is optional and returns the fullscreen banner Ad unit ID; due to AdMob limitations it cannot be the same Ad unit used for native Ads. - `bottomBannerAdUnit` is optional and returns the Clips bottom banner Ad unit ID. - `customKvps` is optional extra key-value targeting data. - `enableBannerAdPriority` is optional; default behavior is native first, then banner fallback if `bannerAdUnit` is configured. When `true`, banner is tried first, then native fallback. - `enableBannerAdPriority` only affects fullscreen native/banner fallback order; it has no effect when `bannerAdUnit` is `nil` and does not change Clips bottom banner behavior. - `Storyteller.shared.modules = [StorytellerAdMobModule(configuration: configuration)]` - Register the AdMob module. Configure only one of `StorytellerGAMModule` or `StorytellerAdMobModule` at a time. ### Default KVPs (only when `enableAdTracking == true`) - Stories: - `stCategories` — Categories associated with the current story - `stCurrentCategory` — Categories of the list containing the story - `stPlacement` — The placement identifier of the story - `stApiKey` — The current Storyteller API key - `stAdIndex` — The order of the ad within the story - Clips: - `stCollection` — The identifier of the clip collection - `stClipCategories` — Categories associated with the current clip - `stNextClipCategories` — Categories of the next clip (if available) - `stApiKey` — The current Storyteller API key - `stAdIndex` — The order of the ad within the clip collection ### Clips opening pre-roll ads - Opening pre-roll is disabled when `StorytellerClipCollectionConfiguration.adConfiguration` is omitted or set to `nil`, and for new `StorytellerClipsAdConfiguration` instances. - To opt a Clips presentation into opening pre-roll, pass `StorytellerClipsAdConfiguration(preRollEnabled: true)` through `StorytellerClipCollectionConfiguration.adConfiguration`. - 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 `initialIndex = 0`. - The request context uses the first opened content Clip as the `clip`, includes the next non-ad Clip when available, and sends `adIndex = 1`. - For GAM and AdMob, this maps to the default `stAdIndex = 1` KVP. - If the CMS-configured opening timeout is reached before the ad loads, the Player starts content and ignores late opening pre-roll results. - Later between-Clip ads continue with the normal cadence and next ad index. ### Custom ads request context - `StorytellerModule.adSource` - Declares the source for custom ads and ad analytics. - Use `.custom("myNetwork")` for custom integrations, `.gam` for Google Ad Manager, `.admob` for Google AdMob; `.storyteller` is reserved. The GAM and AdMob modules set this automatically, and the VAST and GAM VAST modules set `.custom("vast")`. - `StorytellerAdRequestInfo` cases: - `stories(placement: String, categories: [String], story: ItemInfo, adIndex: Int)` - `placement`: placement identifier of the Story. - `categories`: categories associated with the list that the Story is part of. - `story`: current Story info. - `adIndex`: order of the ad within the current playback session, starting from `1`. - `clips(collection: String, clip: ItemInfo, nextClip: ItemInfo?, adIndex: Int)` - `collection`: clip collection identifier. - `clip`: current Clip info. - `nextClip`: next Clip info, if available. - `adIndex`: order of the ad within the displayed ads in the collection (`1` for the first ad, `2` for the second, etc.). - `ItemInfo` includes: - `categories` — an array of `StorytellerCategory` objects representing categories that the Story or Clip is part of. ## Examples Basic GAM setup (ad unit selection): ```swift import StorytellerSDK import StorytellerGAMIntegration let configuration = StorytellerGAMModuleConfiguration( adUnit: { requestInfo in return "YOUR_AD_UNIT_ID" } ) ``` Register the GAM module: ```swift Storyteller.shared.modules = [StorytellerGAMModule(configuration: configuration)] ``` Dynamic ad units for Stories vs Clips: ```swift import StorytellerSDK import StorytellerGAMIntegration let configuration = StorytellerGAMModuleConfiguration( adUnit: { requestInfo in switch requestInfo { case .stories: return "/33813572/storyteller/stories" case .clips: return "/33813572/storyteller/clips" } } ) ``` Additional parameters (custom templates + PPID + custom KVPs + request callback): ```swift import GoogleMobileAds import StorytellerSDK import StorytellerGAMIntegration let configuration = StorytellerGAMModuleConfiguration( adUnit: { requestInfo in switch requestInfo { case .stories: return "/33813572/storyteller/stories" case .clips: return "/33813572/storyteller/clips" } }, customNativeTemplateIds: StorytellerGAMModuleConfiguration.CustomNativeTemplateIds( stories: "YOUR_STORIES_TEMPLATE_ID", clips: "YOUR_CLIPS_TEMPLATE_ID" ), publisherProvidedId: { "YOUR_PUBLISHER_PROVIDED_ID" }, customKvps: { [ "YOUR_CUSTOM_KEY": "YOUR_CUSTOM_VALUE", "YOUR_PRIVACY_KEY": "YOUR_PRIVACY_VALUE" ] }, configureAdRequest: { requestInfo, request in let bidResponse = await yourSliideBidder.loadDemand( requestInfo: requestInfo, targeting: request.customTargeting ?? [:] ) bidResponse.applyAPSDemand(to: request) bidResponse.applyNimbusDemand(to: request) } ) ``` In this example, `yourSliideBidder` is your app-owned adapter around your Sliide, APS, or Nimbus integration. Replace `loadDemand`, `applyAPSDemand`, and `applyNimbusDemand` with the APIs from the bidder SDKs you use. `configureAdRequest` is called on the main actor for full-screen native, custom-template, banner-fallback, and bottom-banner GAM requests after Storyteller applies its default `st*` KVPs, host `customKvps`, and any `publisherProvidedId`, but before the Google request is loaded. Storyteller awaits this callback 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. The callback is not automatically privacy-gated by Storyteller; apply required consent or limited-ad-tracking checks 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 `st` are reserved. If the callback is omitted, Storyteller builds and loads the request using the existing request behavior. Basic GAM VAST setup: ```swift import StorytellerSDK import StorytellerVASTIntegration let configuration = StorytellerGAMVASTModuleConfiguration( adUnit: { requestInfo in switch requestInfo { case .stories: return "/33813572/storyteller/stories_vast" case .clips: return "/33813572/storyteller/clips_vast" } }, descriptionUrl: { _ in "https://example.com/storyteller-video" }, contentUrl: { _ in "https://example.com/storyteller-video" }, customParams: { _ in ["sliide_content_category": "sports"] } ) Storyteller.shared.modules = [StorytellerGAMVASTModule(configuration: configuration)] ``` Basic AdMob setup: ```swift import StorytellerSDK import StorytellerGAMIntegration let configuration = StorytellerAdMobModuleConfiguration( adUnit: { requestInfo in return "YOUR_NATIVE_AD_UNIT_ID" }, bannerAdUnit: { requestInfo in return "YOUR_BANNER_AD_UNIT_ID" }, bottomBannerAdUnit: { requestInfo in return "YOUR_BOTTOM_BANNER_AD_UNIT_ID" } ) Storyteller.shared.modules = [StorytellerAdMobModule(configuration: configuration)] ``` AdMob test IDs for local validation: | Placement | Sample AdMob unit ID | |-----------|----------------------| | Native | `ca-app-pub-3940256099942544/3986624511` | | Native video | `ca-app-pub-3940256099942544/2521693316` | | Fullscreen banner fallback | `ca-app-pub-3940256099942544/2435281174` | | Clips bottom banner | `ca-app-pub-3940256099942544/2934735716` | ## Pitfalls / Notes - Import the same version of `StorytellerSDK` and `StorytellerGAMIntegration`. - Import the same version of `StorytellerSDK` and `StorytellerVASTIntegration` when using VAST. - Only one ads integration module can be used at a time; `StorytellerGAMModule` and `StorytellerAdMobModule` are mutually exclusive. - Default KVPs are only sent when ad tracking is enabled (`enableAdTracking == true` in the `StorytellerEventTrackingOptions` passed to `Storyteller.shared.initialize(...)`). - `publisherProvidedId` sends PPID through Google Ad Manager's request-level PPID field; return `nil` when your app should not send PPID. `customKvps` is not merged with any KVPs configured elsewhere in your app. - `configureAdRequest` runs on the main actor, can await host bidder work before load, and is not automatically privacy-gated by Storyteller; apply any required consent checks before mutating the Google request and do not overwrite Storyteller-owned `st*` targeting keys. - Bottom banner ads for Clips require providing `bottomBannerAdUnit` (leave it `nil` if you don’t plan to serve this placement) and opting the Clips presentation in with `StorytellerClipsAdConfiguration(bottomBannerEnabled: true)`. - For AdMob, native Ads and fullscreen banner Ads require separate Ad units; `enableBannerAdPriority` defaults to native-first behavior and only changes the fullscreen native/banner order when `bannerAdUnit` is configured. - The VAST module requires HTTPS tag URLs and generated URLs no longer than 2,048 bytes. - The VAST module is generic VAST support; do not hard-code ITV-specific parameter names or production URLs into host integrations. - The VAST module supports compatible linear VAST 2.x, 3.x, and 4.x responses, ignores unsupported creative types such as VPAID JavaScript, and does not support bottom banner ads. - `StorytellerGAMVASTModule` is part of `StorytellerVASTIntegration`; it does not use the Google Mobile Ads SDK or IMA SDK. - For GAM VAST, put custom targeting in `customParams`; put extra GAM ad tag parameters in `tagParameters`. - `tagParameters` are applied after SDK-generated GAM VAST parameters, so avoid overriding generated keys unless that is intentional. - Non-skippable ads (an enforced period during which ads can’t be skipped) are configured in the CMS. ## Cross-References - `AdditionalMethods.md` - `Analytics.md` - `PrivacyAndTracking.md` - `StorytellerDelegate.md` # Deep linking URL: /Deeplinking/ ## Task You are helping an engineer integrate Storyteller. Use this documentation to answer questions, propose improvements, and point out missing details. Be precise and call out assumptions. ## Metadata - Slug: `deeplinking` - Source: `public-docs/Deeplinking.md` - Audience: iOS SDK integrators - Platforms: `iOS` - Related: `OpenPlayer.md` ## Overview - To support sharing and deep links, your app needs to handle Storyteller deep link URLs. - iOS supports Universal Links (HTTPS) and custom URL schemes; they serve different use cases and use formats like `https://[tenant_name].shar.estori.es/...` and `[tenant_name]stories://...`. - For push notifications on iOS, you must use URL scheme links (not Universal Links) to open the app reliably. - UIKit receives links through app- or scene-delegate lifecycle methods; SwiftUI receives both Universal Links and custom URL schemes through `.onOpenURL`. - Use `Storyteller.shared.isStorytellerDeepLink(url:)` to detect Storyteller links and `Storyteller.shared.openDeepLink(url:)` to open the requested content. ## When To Use - Universal Links: when you don’t know if the app is installed (social/email sharing). - URL Scheme Links: when you know the app is installed (push notifications, in-app routing). ## Integration Steps ### Universal Links (HTTPS) 1. Add Associated Domains capability in Xcode. 2. Add: - `applinks:[tenant_name].ope.nstori.es` - `applinks:[tenant_name].shar.estori.es` 3. Add your app’s bundle identifier to Storyteller CMS (Apps → iOS app entry → App ID `.`). ### Custom URL Scheme 1. Register a URL scheme in Xcode: `[TENANT_NAME]stories` (format: `[TENANT_NAME]stories://`). ### App Handling 1. Choose one framework route: - UIKit app lifecycle: receive Universal Links through `application(_:continue:restorationHandler:)` and custom schemes through `application(_:open:options:)`. - UIKit scene lifecycle: inspect `UIScene.ConnectionOptions` in `scene(_:willConnectTo:options:)` at cold start, then receive later Universal Links through `scene(_:continue:)` and custom schemes through `scene(_:openURLContexts:)`. - SwiftUI: apply `.onOpenURL` to a stable root view for both link types. 2. In either route, validate with `isStorytellerDeepLink(url:)` and then call `openDeepLink(url:)`. 3. Test one HTTPS Universal Link and one tenant custom-scheme link. ### Link Formats (examples) - Story Category: - `https://[tenantname].shar.estori.es/go/category/123456` - `[tenantname]stories://open/category/123456` - Clip Collection: - `https://[tenantname].shar.estori.es/open/clip/CLIP_UUID?collectionId=COLLECTION_ID&categoryId=CATEGORY_ID` - `[tenantname]stories://open/clip/CLIP_UUID?collectionId=COLLECTION_ID&categoryId=CATEGORY_ID` - Story / Page: - `https://[tenantname].shar.estori.es/story/STORY_UUID` - `https://[tenantname].shar.estori.es/page/PAGE_UUID` - `[tenantname]stories://open/STORY_UUID/PAGE_UUID` - Sheet: - `https://[tenantname].ope.nstori.es/open/sheet/SHEET_ID` - `[tenantname]stories://open/sheet/SHEET_ID` ## API Cheat Sheet - `Storyteller.shared.isStorytellerDeepLink(url: URL) -> Bool` - Returns `true` if a URL is a Storyteller deep link. - `Storyteller.shared.openDeepLink(url: URL) async throws` - Opens the Story/Clip/Sheet specified by the deep link. - Throws when the content can’t be opened (for example it’s not available). - Manual deep link handling option: - If you need more control, parse the URL yourself (after `isStorytellerDeepLink`) and call specific methods like `openStory(id:)`, `openPage(id:)`, `openCollection(configuration:)`, `openCategory(category:)`, or `openSheet(id:)` (see `OpenPlayer.md`). ## Examples Helper methods: ```swift Storyteller.shared.isStorytellerDeepLink(url: URL) -> Bool ``` ```swift Storyteller.shared.openDeepLink(url: URL) async throws ``` UIKit AppDelegate (Universal Links and custom schemes): ```swift import UIKit import StorytellerSDK final class AppDelegate: UIResponder, UIApplicationDelegate { func application( _ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void ) -> Bool { guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL else { return false } return openStorytellerURL(url) } func application( _ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:] ) -> Bool { openStorytellerURL(url) } private func openStorytellerURL(_ url: URL) -> Bool { guard Storyteller.shared.isStorytellerDeepLink(url: url) else { return false } Task { @MainActor in do { try await Storyteller.shared.openDeepLink(url: url) } catch { print("Unable to open Storyteller link: \(error.localizedDescription)") } } return true } } ``` UISceneDelegate is the alternative UIKit route for both link types when the app owns lifecycle handling there: ```swift import UIKit import StorytellerSDK final class SceneDelegate: UIResponder, UIWindowSceneDelegate { func scene( _ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions ) { if let url = connectionOptions.userActivities.lazy .filter({ $0.activityType == NSUserActivityTypeBrowsingWeb }) .compactMap(\.webpageURL) .first(where: { Storyteller.shared.isStorytellerDeepLink(url: $0) }) { openStorytellerURL(url) return } guard let url = connectionOptions.urlContexts.lazy .map(\.url) .first(where: { Storyteller.shared.isStorytellerDeepLink(url: $0) }) else { return } openStorytellerURL(url) } func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL else { return } openStorytellerURL(url) } func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { guard let url = URLContexts.lazy .map(\.url) .first(where: { Storyteller.shared.isStorytellerDeepLink(url: $0) }) else { return } openStorytellerURL(url) } private func openStorytellerURL(_ url: URL) { guard Storyteller.shared.isStorytellerDeepLink(url: url) else { return } Task { @MainActor in do { try await Storyteller.shared.openDeepLink(url: url) } catch { print("Unable to open Storyteller link: \(error.localizedDescription)") } } } } ``` SwiftUI root handling for both link types: ```swift import SwiftUI import StorytellerSDK struct StorytellerAppRootView: View { var body: some View { ContentView() .onOpenURL { url in openStorytellerURL(url) } } private func openStorytellerURL(_ url: URL) { guard Storyteller.shared.isStorytellerDeepLink(url: url) else { return } Task { @MainActor in do { try await Storyteller.shared.openDeepLink(url: url) } catch { print("Unable to open Storyteller link: \(error.localizedDescription)") } } } } ``` Push notification payload (must use custom scheme link): ```json { "aps": { "alert": { "title": "Check out this story!", "body": "Tap to view the latest content" } }, "deeplink_url": "[tenant_name]stories://open/STORY_ID/PAGE_ID" } ``` Extracting deep links from push notifications (`UNUserNotificationCenterDelegate`): ```swift func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async { let userInfo = response.notification.request.content.userInfo guard let deepLink = userInfo["deeplink_url"] as? String, let url = URL(string: deepLink) else { return } // This triggers the existing onOpenURL or application(_:open:options:) route. await UIApplication.shared.open(url) } ``` ## Pitfalls / Notes - Push notifications on iOS do not support Universal Links for directly opening apps; use URL scheme links in push payloads. - `Storyteller.shared.openDeepLink` can accept both HTTPS links (Associated Domains) and custom scheme links. - `Storyteller.shared.openDeepLink` parses URLs and routes to internal equivalents for: - Story Category (`/open/category/` or `/go/category/`) - Clip Collection (`/open/clip`, `/go/clip`, `/open/clips`, `/go/clips`, with required `collectionId`, optional `categoryId`, and an optional `clipId` path segment) - Story/Page (patterns like `/story/STORY_ID`, `/page/PAGE_ID`, or `open/STORY_ID/PAGE_ID`) - Sheet (`/open/sheet/` or `/go/sheet/`) ## Cross-References - Showcase SwiftUI `.onOpenURL` route: https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/main/ShowcaseApp/ShowcaseApp.swift#L37 - Showcase deep link handler: https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/main/ShowcaseApp/ShowcaseApp.swift#L152 - UIKit CocoaPods sample AppDelegate: https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/cocoapods/StorytellerSampleApp/AppDelegate.swift#L5 # Navigating to App URL: /NavigatingToApp/ ## Task You are helping an engineer integrate Storyteller. Use this documentation to answer questions, propose improvements, and point out missing details. Be precise and call out assumptions. ## Metadata - Slug: `navigating-to-app` - Source: `public-docs/NavigatingToApp.md` - Audience: iOS SDK integrators - Platforms: `iOS` - Related: `StorytellerDelegate.md`, `Deeplinking.md` ## Overview - `StorytellerDelegate` includes `userNavigatedToApp(url: String)` for in-app navigation triggered by Storyteller action buttons configured as `deeplink` in the CMS. - The callback is only for app-defined `deeplink` actions configured in the CMS, not for opening Storyteller content. - When a user taps a matching action button, the SDK passes the raw CMS-configured URL string to your app. - Your app is responsible for parsing that URL and routing to the appropriate view. ## When To Use - You want Storyteller CMS pages/actions to deep link into screens inside your app (non-Storyteller screens). - You’re already using Storyteller action buttons and need custom navigation handling for `deeplink` URLs. ## Integration Steps 1. Implement `StorytellerDelegate` in your app. 2. Implement `userNavigatedToApp(url:)` to parse CMS-provided `deeplink` URLs and route within your app. 3. Ensure `Storyteller.shared.delegate` is set to your delegate implementation. ## API Cheat Sheet - `StorytellerDelegate.userNavigatedToApp(url: String)` - Called when a user taps an action button whose link type is set to `deeplink` in the CMS. - `url` is the raw URL string as configured in Storyteller CMS; parse and route inside your app. ## Examples Minimal implementation: ```swift func userNavigatedToApp(url: String) { // parse the url and navigate to the destination } ``` ## Pitfalls / Notes - Treat the passed `url` as an external input: validate and handle unknown routes safely. - This is for navigating within your app; for opening Storyteller content via deep links, see `Deeplinking.md`. ## Cross-References - Showcase delegate implementation: https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/main/ShowcaseApp/Storyteller/StorytellerInstanceDelegate.swift#L38 - Showcase router: https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/main/ShowcaseApp/Router.swift#L50 # Search URL: /Search/ ## Task You are helping an engineer integrate Storyteller. Use this documentation to answer questions, propose improvements, and point out missing details. Be precise and call out assumptions. ## Metadata - Slug: `search` - Source: `public-docs/Search.md` - Audience: iOS SDK integrators - Platforms: `iOS` - Related: `Themes.md`, `AdditionalMethods.md` ## Overview - Storyteller Search lets users search for Stories and Clips and shows suggestions while typing. - Search results are grouped into `Stories` and `Clips`. - Search must be enabled for your tenant by the Storyteller team; when enabled it appears inside the Story and Clip players. - `Storyteller` exposes app-level support: `isSearchEnabled` and `openSearch`. - `openSearch` can be triggered from anywhere; if the Storyteller player is currently displayed, it will be dismissed before presenting Search. - `openSearch()` is async and non-throwing. When Search is disabled, it returns without presenting anything. - The Search background, input, filter button, suggestions, no-results state and filter sheet can be customized independently for light and dark themes; omitted appearance properties retain the existing UI (`Themes.md#search`). ## When To Use - You want an in-player search experience for Stories and Clips. - You want to open Search from your own UI (for example, a “Search” button in your app header). ## Integration Steps 1. Confirm Search is enabled for your tenant. 2. Check `isSearchEnabled` to decide whether to show entry points to Search. 3. Call `openSearch` when you want to present Search. 4. (Optional) Customize Search visuals via Themes. ## API Cheat Sheet - `Storyteller.shared.isSearchEnabled` - Returns whether Search functionality is enabled at the app level. - `Storyteller.shared.openSearch()` - Opens the Search component from anywhere in the app. - If the Storyteller player is currently displayed, it is dismissed before Search is presented. - Silently returns if Search is disabled for the tenant. ### Filters ### Date Posted - `All` (default) - `Past 24 hours` - `Last Week` - `Last Month` - `Last Year` ### Content Type - `All` (default) - `Stories` - `Clips` ### Sort By - `Relevance` (default) - `Like Count` - `Share Count` - `Date Posted` ## Examples Open Search from host UI while providing a disabled-state diagnostic: ```swift import StorytellerSDK func searchButtonTapped() { Task { @MainActor in guard Storyteller.shared.isSearchEnabled else { print("Storyteller Search is not enabled for this tenant") return } await Storyteller.shared.openSearch() } } ``` ## Pitfalls / Notes - Search must be enabled by the Storyteller team for your tenant; `isSearchEnabled` reflects the app-level state, and `openSearch()` is a silent no-op when disabled. ## Cross-References - Showcase `openSearch` trigger: https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/main/ShowcaseApp/Views/Home/HomeView.swift#L252 - Search theme customization: `Themes.md#search` # Storyteller Brightcove Module URL: /Brightcove/ ## Task You are helping an engineer integrate Storyteller. Use this documentation to answer questions, propose improvements, and point out missing details. Be precise and call out assumptions. ## Metadata - Slug: `brightcove` - Source: `public-docs/Brightcove.md` - Audience: iOS SDK integrators - Platforms: `iOS` - Related: ## Overview - The Brightcove integration records `AVPlayer` events and forwards them to Brightcove analytics. - Install via SPM and add the `StorytellerBrightcoveIntegration` target to your app. - Requires StorytellerSDK version `11.0.0` or higher. - Configure the module and append it to `Storyteller.shared.modules`. ## When To Use - You need Brightcove analytics for playback within Storyteller content. ## Integration Steps 1. Add the SPM dependency: `https://github.com/getstoryteller/storyteller-brightcove-collector-swift`. 2. Add the `StorytellerBrightcoveIntegration` target to your app target. 3. Create a `StorytellerBrightcoveModuleConfiguration` and append a `StorytellerBrightcoveModule` to `Storyteller.shared.modules`. ## API Cheat Sheet - `StorytellerBrightcoveModuleConfiguration(account:playerName:source:destination:)` - Configuration payload for Brightcove analytics (account + identifiers). `playerName` is optional. - `StorytellerBrightcoveModule(configuration:)` - Module instance you register with Storyteller. - `Storyteller.shared.modules.append(...)` - Append the Brightcove module alongside any other modules you register. ## Examples Module configuration + registration: ```swift import StorytellerBrightcoveIntegration func initializeStoryteller() { let brightcoveConfiguration = StorytellerBrightcoveModuleConfiguration( account: "", playerName: "Video Player", // optional source: "", destination: "" ) Storyteller.shared.modules.append(StorytellerBrightcoveModule(configuration: brightcoveConfiguration)) // other Storyteller initialization code } ``` ## Cross-References - Showcase example: https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/main/ShowcaseApp/ShowcaseApp.swift#L95 # Open Player URL: /OpenPlayer/ ## Task You are helping an engineer integrate Storyteller. Use this documentation to answer questions, propose improvements, and point out missing details. Be precise and call out assumptions. ## Metadata - Slug: `open-player` - Source: `public-docs/OpenPlayer.md` - Audience: iOS SDK integrators - Platforms: `iOS` - Related: `EmbeddedClips.md`, `Deeplinking.md`, `AdditionalMethods.md` ## Overview - Use these methods to programmatically open Storyteller to specific content: Stories, Pages, Categories, and Clip Collections. - All documented open APIs are `async throws` and should be called from an async context such as a `Task`. - `openReason` is optional and is used for analytics attribution only. - Story APIs can open by Story ID, Story external ID, Page ID, or Story category ID. - Clips APIs can open an entire collection or attempt to jump directly to a Clip by external ID. ## When To Use - You want to open specific Storyteller content from your own UI (tiles, banners, notifications, etc). - You have IDs/external IDs and want to deep link directly into a Story, Page, Category, or Clip Collection. ## Integration Steps 1. Ensure the SDK is initialized (`Storyteller.shared.initialize(...)`). 2. Call the relevant open method inside a `Task` (these APIs are async). 3. Handle errors (content not available, invalid IDs, etc). ## API Cheat Sheet ### Stories & Categories - `func openCategory(category: String, openReason: StorytellerOpenReason = .instanceMethod) async throws` - Opens a list of Stories filtered by a category ID. - `func openStory(id: String, openReason: StorytellerOpenReason = .instanceMethod) async throws` - Opens a single Story by Story ID. - `func openStory(externalId: String, openReason: StorytellerOpenReason = .instanceMethod) async throws` - Opens a single Story by external ID. - `func openPage(id: String, openReason: StorytellerOpenReason = .instanceMethod) async throws` - Opens a specific Page by Page ID; the SDK deduces the owning Story. ### Clips & Collections - `func openCollection(configuration: StorytellerClipCollectionConfiguration, openReason: StorytellerOpenReason = .instanceMethod) async throws` - Opens a Clip collection, optionally starting at a specific Clip or Category via `configuration.destination`. - If `configuration.destination` is provided but not found, opens the first clip in the collection. - `func openClipByExternalId(collectionId: String, externalId: String, openReason: StorytellerOpenReason = .instanceMethod) async throws` - Opens a Clip collection and attempts to navigate directly to a Clip by external ID. - If the external ID is not found, opens the first clip in the collection. ### Open reason - `StorytellerOpenReason` - Used only for analytics; no functional effect on player behavior. ## Examples Open a Story and handle failures: ```swift import StorytellerSDK func openFeaturedStory() { Task { @MainActor in do { try await Storyteller.shared.openStory(id: "featured-story") } catch { print("Unable to open Story: \(error.localizedDescription)") } } } ``` Open a Clip collection with analytics context: ```swift import StorytellerSDK func openTopPlays() { let configuration = StorytellerClipCollectionConfiguration( collectionId: "top-plays", context: ["location": "home"] ) Task { @MainActor in do { try await Storyteller.shared.openCollection(configuration: configuration) } catch { print("Unable to open Clips: \(error.localizedDescription)") } } } ``` ## Pitfalls / Notes - All methods are `async throws`; call them from an async context and handle failures. - `openReason` is for analytics attribution only. ## Cross-References - Showcase `openCategory` usage: https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/main/ShowcaseApp/Views/Home/Components/FeedImageView.swift#L3 # Additional Methods URL: /AdditionalMethods/ ## Task You are helping an engineer integrate Storyteller. Use this documentation to answer questions, propose improvements, and point out missing details. Be precise and call out assumptions. ## Metadata - Slug: `additional-methods` - Source: `public-docs/AdditionalMethods.md` - Audience: iOS SDK integrators - Platforms: `iOS` - Related: `OpenPlayer.md`, `Deeplinking.md`, `Search.md`, `Ads.md`, `Analytics.md`, `PrivacyAndTracking.md`, `StorytellerDelegate.md`, `StorytellerModule.md`, `Users.md` ## Overview - `Storyteller.shared` is the entry point; this page lists commonly-used properties and utility methods. - Call `Storyteller.shared.initialize(apiKey:userInput:eventTrackingOptions:)` early in the app lifecycle (`async throws`). - `eventTrackingOptions` is configured during initialization and can only be changed by reinitializing. - `delegate` + `modules` are key integration hooks (navigation, ads, etc). - `dismissPlayer(animated:dismissReason:)` force-closes the current player. A `nil` reason leaves `dismissedReason` unset; it does not suppress an otherwise-enabled activity callback. - `openSearch()` opens Search when `isSearchEnabled == true` and otherwise returns without presenting anything; `openSheet(id:)` is `async throws`. ## When To Use - You need a quick index of `Storyteller.shared` state and utility methods during integration. - You need to open or close Storyteller screens programmatically (Search, Sheet, dismiss player). ## API Cheat Sheet ### Key properties - `Storyteller.shared.delegate` - Handles integration callbacks such as navigation and ads; see `StorytellerDelegate.md`. - `Storyteller.shared.modules` - List of `StorytellerModule` instances that extend Storyteller (for example host-supplied ads); see `StorytellerModule.md`. - `Storyteller.shared.currentApiKey` - Current API key used for the most recent `initialize(...)` call. - `Storyteller.shared.version` - SDK version string (for example `"11.4.0"`). - `Storyteller.shared.isInitialized` - Becomes `true` after `initialize(...)` completes successfully; each new initialization call first resets it to `false`. - `Storyteller.shared.isPresentingContent` - `true` while a Story Player, Clip Player, Search, or Sheet is visible; `false` otherwise. - `Storyteller.shared.isPlayerMuted` - Main-actor-isolated instance property reporting the current Player mute state. - `Storyteller.shared.isSearchEnabled` - `true` when the Search feature is enabled in tenant configuration. - `Storyteller.shared.theme` - Default fallback theme used for rendering Stories/Clips items in lists and activities launched from lists. - `Storyteller.shared.user` - Manages user custom attributes for personalization and audience targeting; see `Users.md`. - `Storyteller.shared.eventTrackingOptions` - Read-only after initialization; configured in `initialize(...)`; see `PrivacyAndTracking.md`. ### Key methods - `func initialize(apiKey: String, userInput: StorytellerUserInput? = nil, eventTrackingOptions: StorytellerEventTrackingOptions = .enableAll) async throws` - Required to use the SDK. Recommended to call as early as possible. - `eventTrackingOptions` defaults to `.enableAll` and can only be changed by reinitializing. - `func dismissPlayer(animated: Bool, dismissReason: String? = nil) async` - Force-closes the current Story or Clips player. - No-op if no player is open. - A supplied `dismissReason` populates the corresponding activity event. `nil` leaves that field unset; callback delivery is instead gated by `enableUserActivityTracking` and, for Ad events, `enableAdTracking`. - `func openSearch() async` - Opens the Search screen when enabled; silently returns when Search is disabled (see `isSearchEnabled`). - `func resumePlayer()` - Resumes the current Story/Clips playback. - No-op if no player is open. - Also use this after dismissing a custom share UI when `Storyteller.shared.useCustomShareHandling == true`. - `func openSheet(id: String) async throws` - Loads and opens a Sheet by ID. - `func getStoriesCount(for categories: [String]) async -> Int` - Returns total count of Stories for a list of category IDs. - `func getClipsCount(for collectionId: String) async -> Int` - Returns total count of Clips in a collection. ## Examples Get the SDK version: ```swift let version = Storyteller.shared.version ``` Set a custom user attribute: ```swift Storyteller.shared.user.setCustomAttribute(key: "location", value: "New York") ``` Read the current Player mute state from the main actor: ```swift @MainActor func updateMuteIndicator() { let isMuted = Storyteller.shared.isPlayerMuted print("Player muted: \(isMuted)") } ``` ## Pitfalls / Notes - Many methods are async; call them from a `Task` or another async context. - `resumePlayer()` and `dismissPlayer(...)` have no effect if no player is currently open. ## Cross-References - `OpenPlayer.md` - `Deeplinking.md` - `Search.md` - `Ads.md` - `Analytics.md` - `PrivacyAndTracking.md` - `StorytellerDelegate.md` - `StorytellerModule.md` - `Users.md` # AI URL: /AI/ ## Task You are helping an engineer integrate Storyteller. Use this documentation to answer questions, propose improvements, and point out missing details. Be precise and call out assumptions. ## Metadata - Slug: `ai` - Source: `public-docs/AI.md` - Audience: iOS SDK integrators - Platforms: `iOS` - Related: ## Overview - Customers with separate access to the private Storyteller iOS Showcase repository can use its `$integrate-storyteller` skill at `.agents/skills/integrate-storyteller/SKILL.md` with compatible AI coding assistants; access guidance is at `https://docs.getstoryteller.com/ios/#showcase-source-access`. - The skill can guide a new integration, audit an existing integration, or troubleshoot a problem using the user's app and the published Storyteller documentation. - Use `https://docs.getstoryteller.com/ios/ai/llms.txt` as the primary Storyteller SDK docs bundle to attach/reference in your AI tool. - For narrower context, discover a topic slug from the aggregate bundle and request its concrete `llms-.txt` filename from the `/ios/ai/` directory. - Prefer referencing/attaching the docs file over pasting (more reliable and less likely to hit context limits). - A practical workflow is to copy the docs bundle into your app repo under `.ai/storyteller-sdk-docs.md`. - Include relevant integration code and any errors so the AI can respond concretely. - If your tool can’t reference a file, paste the relevant sections from the docs. ## When To Use - You have the Storyteller iOS Showcase repository and want its integration skill to inspect your app. - You want AI help integrating Storyteller with answers grounded in the official docs. - You’re troubleshooting an integration and want the AI to stay aligned with current SDK behavior. - You want a repeatable, repo-local docs file your team can reference in prompts. ## Integration Steps Skill route: 1. If your authorised GitHub account has private Showcase access, open the Storyteller iOS Showcase repository in your assistant's workspace alongside your iOS app. The skill is at `.agents/skills/integrate-storyteller/SKILL.md`. 1. Ask: "Use `$integrate-storyteller` to help me integrate Storyteller into ``." 1. For an existing integration, ask the skill to audit the app instead. Documentation-bundle route: 1. Download `https://docs.getstoryteller.com/ios/ai/llms.txt`. 1. Create a `.ai/` directory in your project 1. Save the downloaded content as `.ai/storyteller-sdk-docs.md` 1. Point your tool to `.ai/storyteller-sdk-docs.md` and reference it in your prompts, or paste the relevant sections into the editor 1. For narrower context, map a `` marker to the concrete `llms-.txt` filename under the `/ios/ai/` directory 1. Use `https://docs.getstoryteller.com/ios/search/search_index.json` to discover concrete documentation pages when needed ## API Cheat Sheet - `https://docs.getstoryteller.com/ios/ai/llms.txt` - Aggregated docs bundle; attach/reference this for most Storyteller SDK questions. - `llms-.txt` - Concrete topic filename derived from a `` marker in the aggregate bundle. - `https://docs.getstoryteller.com/ios/search/search_index.json` - Route-discovery index generated with the documentation site. - `.ai/storyteller-sdk-docs.md` - Recommended project-local copy of the aggregate bundle for tools that can reference files in-repo. ## Examples Prompt examples: 1. "Using the Storyteller SDK documentation, help me initialize the SDK in my Swift iOS app." 1. "Based on the Storyteller documentation, what's the best way to implement a StorytellerStoriesRow SwiftUI view in my app?" 1. "Help me troubleshoot this initialization error with Storyteller SDK: [paste your error]" 1. "Using the Storyteller docs context, show me how to customize the theme of my StorytellerStoriesRow" 1. "With the Storyteller documentation, help me implement analytics tracking for story views." ## Cross-References - Showcase source access: `https://docs.getstoryteller.com/ios/#showcase-source-access` - Storyteller Showcase App: https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/main/ShowcaseApp/ShowcaseApp.swift#L19