Start with what the app can observe. An empty component, a missing analytics event, and an Ad that was not requested may look similar on screen, but they have different owners and next checks.
If you have not yet displayed your first Story row, follow the complete iOS Quickstart Guide first. For Apple TV, use the dedicated tvOS Guide.
A load is requested but no completion callback arrives
Confirm initialization completed successfully, the callback recipient is retained, any deferred list identifier remained unchanged, and the request is not blocked.
The load reaches a success, empty, or failure callback.
App lifecycle or configuration, network, or SDK request path
Appearance or configuration changes have no effect
Check global versus per-component configuration, the active light/dark theme, any supported remote override, and whether the component was reconfigured or reloaded where required.
A deliberately visible change appears after the active host and remote precedence layers are established.
App or tenant/CMS configuration, or an unsupported SDK customization
Follow the instructions for the installation method you actually use:
With Swift Package Manager, add the StorytellerSDK product to the app target. Its Storyteller Lottie dependency resolves transitively.
With CocoaPods, include both Storyteller spec sources, run pod install, and open the generated .xcworkspace.
With manual frameworks, add both StorytellerSDK.xcframework and StorytellerLottie.xcframework to the app target and select Embed & Sign for both.
When using a Storyteller Ads extension, use compatible versions of the core SDK and extension and import the extension product separately.
The expected result is that the app imports, links, and launches with the SDK before initialize(...) runs. If a documented clean installation fails in a minimal app, capture the package resolution or linker error and get help from Storyteller. See Quickstart installation for the complete setup.
Storyteller.shared.initialize(...) is async throws. The clearest integration sequence is to await it and retain the original error before loading content:
Confirm the API key identifies the intended tenant and environment.
Confirm the current device can reach the required network services and that the app's network policy is not blocking the request.
Inspect the error and Storyteller.shared.isInitialized after the call finishes.
Initialization can throw StorytellerError.networkError(Error) for an unsuccessful server response. It can also surface an underlying URL, transport, or response-decoding error, so keep a general error path. After a thrown initialization call, isInitialized remains false; after success, it is true.
If the same key initializes in the intended environment but one component fails later, initialization is no longer the failing boundary. Continue with Content Fails to Load.
An error or success == false means the request did not produce a successful content result. It is not the same as a successful response containing no available items.
Record the returned error and whether initialization had already succeeded.
Capture Storyteller SDK logs through StorytellerDelegate.log(message:) on the strongly retained object assigned to Storyteller.shared.delegate. Reproduce the failure and look for the request URL, transport error, or response-parsing error that corresponds to the callback. SDK request URLs can include a hashed user ID when enableRemoteViewingStore is enabled, even when personalization is disabled; they can also include custom-attribute values when personalization is enabled. Redact those values before forwarding logs to a third-party service or sharing them.
Confirm the category or collection identifier was copied from the same Storyteller tenant as the API key. When the app has separate development and production configurations, check that the key and identifier came from the same configuration.
If initialization also fails, return to Initialization Fails. If initialization succeeds and the same request repeatedly fails with valid inputs and connectivity, retain the error and request details and get help from Storyteller.
For lists, success == true, no error, and dataCount == 0 establishes that the request completed but returned no items available to this component and targeting context.
Check:
The category or collection identifier is the intended one and belongs to the same tenant and environment as the API key.
The content is assigned to that category or collection and is published rather than draft.
The publication schedule has started and the content has not expired.
When availability depends on an external user ID or custom attributes, Storyteller.shared.eventTrackingOptions.enablePersonalization was enabled during initialization. When it is disabled, those targeting values are omitted from supported content requests. The standard userId query parameter on those requests also requires enableRemoteViewingStore; remote-viewing requests follow the separate contract described in Privacy and Tracking.
The external user ID, custom attributes, locale, followed-category state, and other targeting inputs make the content available.
The selected component and configuration are intended for that content type.
The expected control is a known, currently available item for the same targeting inputs and environment. If that appears, the original result belongs to content or targeting configuration rather than layout. See Working with Users for external IDs and targeting inputs, Privacy and Tracking for initialization-time personalization settings, and Storyteller List Views for list identifiers and configuration.
For lists, success == true with dataCount > 0 establishes that content loaded. Diagnose presentation next:
Give a UIKit row or grid valid Auto Layout constraints and a non-zero size.
Give a SwiftUI row an appropriate non-zero frame in its parent layout.
Confirm the component is in the visible view hierarchy and is not hidden by host-app state.
Check configuration that changes list visibility or sizing, such as visibleTiles, before forcing a fixed height.
The expected result is that the already loaded items become visible without changing the content identifier. Start from the working layouts in the iOS Quickstart Guide, then move to Storyteller List Views for advanced layouts.
Story and Clips lists and Cards can defer a load requested before initialization. This is a safeguard, not the recommended integration sequence; await initialization explicitly. A deferred Story or Clips list reload continues only when its list identifier remains unchanged while it waits. Cards wait for initialization without that identifier check.
If the app requests a load but no completion arrives:
Confirm initialize(...) eventually completed successfully. If an attempt throws, the deferred load remains pending and can continue after a later successful retry.
For a Story or Clips list requested before initialization, confirm its identifier did not change while waiting. If it changed, the SDK abandons that deferred reload before onDataLoadStarted() or onDataLoadComplete(...); after initialization succeeds and the deferred attempt unwinds, request a new reload with the current identifier.
Do not issue another Story or Clips list reload while an earlier reload is in progress, including while it waits for initialization. The later call is ignored and does not produce its own onDataLoadStarted() or onDataLoadComplete(...); wait for the earlier attempt to finish or unwind before retrying.
Assign the delegate or action before starting the load and keep any weak delegate strongly referenced by app-owned state.
Confirm the device remains online and use the SDK log callback and network diagnostics to inspect whether a request starts or stalls.
Reduce the integration to one known content identifier and one component.
The expected result is a terminal success, empty, or failure callback. If no request starts, focus on app lifecycle and configuration. If a request starts but never finishes under repeatable network conditions, capture the last observed request boundary and get help from Storyteller.
Assign the surface's delegate or action before loading; strongly retain weak delegates.
A load-start or completion result arrives independently of analytics settings.
Player interaction and app navigation
Assign and strongly retain Storyteller.shared.delegate; the SDK property is weak.
The implemented callback arrives when its corresponding interaction occurs.
Analytics through onUserActivityOccurred
Set tracking options during initialization and keep enableUserActivityTracking enabled.
Supported non-Ad interactions produce user-activity events.
Ad analytics through onUserActivityOccurred
Also keep enableAdTracking enabled and reach an actual Ad opportunity or interaction.
Ad events arrive only when an Ad lifecycle event occurs.
eventTrackingOptions can only be selected during initialization. Reinitialize the SDK to change them. enableUserActivityTracking gates onUserActivityOccurred; disabling enableAdTracking additionally suppresses Ad-related analytics and omits default KVPs and customKvps from supported Google Ad requests.
Tracking options govern analytics delivery; they do not determine whether a content request or provider Ad request is made. Use the callback belonging to the affected surface before changing privacy settings.
Storyteller.shared.theme is the global fallback theme. Set it early in the app lifecycle.
A theme passed in a list configuration applies to that component instead of the global fallback.
For properties supported by remote appearance settings, a feed- or collection-specific remote value overrides the tenant/global remote value, which overrides the host-supplied light or dark theme. Confirm the active CMS values before treating a host customization as an SDK gap.
The list's uiStyle selects the light or dark theme branch. For a universal customization, configure theme.light, copy it with theme.dark = theme.light, then apply any dark-specific overrides.
After changing list configuration, reconfigure and reload the UIKit view, or update and reload the SwiftUI model, as described in Storyteller List Views.
Test one deliberately visible property on a basic component before combining multiple overrides.
If the change works on the basic component but not the production surface, compare per-component overrides, the active appearance branch, and configuration timing. If the documented property never affects its supported surface in a minimal integration, record the property, surface, and active theme branch as a possible SDK gap.
Use Custom Themes for supported properties and their scopes. The troubleshooting route does not imply that every visual detail is customizable.
Ads cross several ownership boundaries. Establish the configured source before debugging the provider:
Storyteller First Party Ads are managed through the Storyteller CMS and require no additional provider integration in the app.
GAM, AdMob, VAST, and GAM VAST Ads require the matching Storyteller extension and provider setup.
Then identify the exact placement:
For fullscreen Story or Clip Ads, confirm the tenant's Ads strategy enables the content type and placement you are testing. For a Clips opening pre-roll, the presentation must also opt in with StorytellerClipsAdConfiguration(preRollEnabled: true).
For a Clips bottom banner, confirm the Storyteller feed response enables the placement with showBottomBannerAd == true, the presentation opts in with StorytellerClipsAdConfiguration(bottomBannerEnabled: true), and the active Ads module supports bottom banners. This placement is separate from the fullscreen and opening pre-roll strategy.
Follow the branch for the configured source and placement:
Configured source
Supported placement and next check
Storyteller First Party Ads
Confirm an eligible Ad is available in the CMS for a fullscreen placement, then observe whether the SDK displays it. First Party Ads do not support Clips bottom banners.
GAM or AdMob Ads
Confirm the app installed and configured the matching extension, then use the extension and provider diagnostics to establish whether the placement produced a provider request. A Clips bottom banner additionally requires bottomBannerAdUnit.
VAST or GAM VAST Ads
Confirm the app installed and configured the matching extension, then inspect its diagnostics for a fullscreen placement. These modules do not support Clips bottom banners.
Continue from the last source-specific observation:
If a provider request starts, inspect its diagnostics or error for no-fill, trafficking, creative, consent, bidder, or network failures.
If the CMS or provider makes an eligible Ad available but Storyteller does not display it, capture the Ad source, returned format when applicable, and subsequent SDK events or logs.
Interpret the last successful boundary:
Last observation
Next owner to check
No eligible First Party Ad is available in the CMS for a fullscreen placement
Tenant Ads strategy, cadence, First Party Ad configuration, or content state
No Clips bottom-banner request
The feed response's showBottomBannerAd value, the presentation's bottomBannerEnabled opt-in, module placement support, or bottomBannerAdUnit
No provider request after an eligible extension-backed placement
External provider setup, inventory, trafficking, consent, bidder, or connectivity
CMS or provider makes an Ad available but it cannot be mapped or rendered
Returned creative/format compatibility or SDK integration/rendering
Tracking settings are not proof that an Ad was requested or rendered. They control analytics delivery and, for supported provider requests, which targeting values are sent; disabling them can therefore affect a line item's ability to match without directly enabling or disabling an Ad placement.
See Ads for provider setup, placement support, and diagnostics. Provider-specific rewrites and live trafficking checks remain in the provider's own integration workflow.
When the documented next check does not resolve the problem, share as much of the following information as is available through your normal Storyteller support channel:
Storyteller SDK version from Storyteller.shared.version and the versions of any Storyteller extension modules.
Installation method, Xcode version, iOS version, and device or simulator model.
Affected surface: UIKit or SwiftUI, Stories, Clips, Cards, Embedded Clips, Player, or Ad placement.
Minimal steps, expected result, actual result, and reproducibility.
The last successful boundary and the complete callback result or error.
Sanitized SDK logs captured through StorytellerDelegate.log(message:), network or provider logs, and a screenshot or short recording when presentation is relevant. Redact hashed user IDs, custom-attribute values, and any other personal data from request URLs before forwarding the logs to a third-party service or sharing them.
API key, tenant/environment name, and relevant content or Ad identifiers.
Whether the same setup succeeds with a known control item, minimal component, or Showcase-equivalent integration.
An SDK defect is more likely when the same supported configuration fails consistently with valid inputs, successful initialization, known available content or a valid provider response, and a minimal reproduction. Identifying the last successful callback or request boundary helps Storyteller distinguish that from an app, tenant, content, or provider problem.
Storyteller API keys and content identifiers can be shared with Storyteller because they identify the tenant and content. SDK request URLs can contain hashed user IDs when enableRemoteViewingStore is enabled, even when personalization is disabled; they can also contain custom-attribute values when personalization is enabled. Redact those values and any other personal data before forwarding or sharing the logs. Do not include access tokens or unredacted provider credentials.
{"slug": "troubleshooting", "page_title": "Troubleshoot an Integration", "page_url": "Troubleshooting/", "canonical_url": "/ios/Troubleshooting/", "markdown": "# Troubleshooting Storyteller on iOS\n\nStart with what the app can observe. An empty component, a missing analytics event, and an Ad that was not requested may look similar on screen, but they have different owners and next checks.\n\nIf you have not yet displayed your first Story row, follow the complete [iOS Quickstart Guide](Quickstart.md) first. For Apple TV, use the dedicated [tvOS Guide](tvOS.md).\n\n## Start with the Observable Result\n\nRecord the first unexpected result before changing the integration:\n\n| Observable result | Check next | Expected observation | Likely owner | Continue with |\n| --- | --- | --- | --- | --- |\n| `StorytellerSDK` cannot be imported, linked, or loaded | Confirm the installation method, app target membership, and required Storyteller dependencies. | The app builds and launches before Storyteller initialization begins. | App dependency setup, or SDK packaging if the documented setup still fails | [Quickstart installation](Quickstart.md#install-the-sdk) |\n| `initialize(...)` throws or `isInitialized` remains `false` | Capture the error, then verify the API key, tenant/environment, and connectivity. | A successful call returns without throwing and sets `isInitialized` to `true`. | App input, tenant/environment, network, or SDK initialization | [Initialization fails](#initialization-fails) |\n| A content load callback reports an error or `success == false` | Inspect the returned error and capture related SDK logs through [`StorytellerDelegate.log(message:)`](StorytellerDelegate.md#log). | The callback distinguishes a failed request from an empty successful response. | App integration, network, tenant, or SDK request path | [Content fails to load](#content-fails-to-load) |\n| A list load succeeds with `dataCount == 0` | Verify the content identifier, publication state, schedule/expiry, targeting inputs, and initialization-time personalization settings. | A known available item from the same tenant and effective targeting context produces a positive count. | App targeting or privacy configuration, tenant, CMS, or content state | [Content is missing or empty](#content-is-missing-or-empty) |\n| A list load succeeds with `dataCount > 0`, but nothing is visible | Check the component's constraints or frame and any visibility-affecting configuration. | Giving the component a non-zero layout reveals the loaded items. | App layout or presentation | [Content loaded but is not visible](#content-loaded-but-is-not-visible) |\n| A load is requested but no completion callback arrives | Confirm initialization completed successfully, the callback recipient is retained, any deferred list identifier remained unchanged, and the request is not blocked. | The load reaches a success, empty, or failure callback. | App lifecycle or configuration, network, or SDK request path | [Loading does not complete](#loading-does-not-complete) |\n| A delegate, action, or analytics callback does not arrive | Identify whether it is a data-load, interaction, or analytics callback, then check its delegate lifetime and tracking gate. | Another callback in the same layer establishes whether loading, interaction, or event delivery is affected. | App callback setup, privacy configuration, tenant Ads setup, or SDK event delivery | [Callbacks or analytics events do not arrive](#callbacks-or-analytics-events-do-not-arrive) |\n| Appearance or configuration changes have no effect | Check global versus per-component configuration, the active light/dark theme, any supported remote override, and whether the component was reconfigured or reloaded where required. | A deliberately visible change appears after the active host and remote precedence layers are established. | App or tenant/CMS configuration, or an unsupported SDK customization | [Appearance or configuration does not change](#appearance-or-configuration-does-not-change) |\n| Ads do not appear | Establish the configured Ad source, whether an eligible placement was reached, and whether the matching extension or provider made a request. | The last observed boundary identifies Storyteller setup, provider delivery, or rendering as the next owner. | Tenant Ads strategy, app extension, external provider, inventory, or SDK rendering | [Ads do not appear](#ads-do-not-appear) |\n\nDo not use a visual symptom alone to assign the cause. Keep the callback result, error, and last successful boundary together.\n\n## Installation or Initialization Fails\n\n### The SDK Does Not Build or Load\n\nFollow the instructions for the installation method you actually use:\n\n- With Swift Package Manager, add the `StorytellerSDK` product to the app target. Its Storyteller Lottie dependency resolves transitively.\n- With CocoaPods, include both Storyteller spec sources, run `pod install`, and open the generated `.xcworkspace`.\n- With manual frameworks, add both `StorytellerSDK.xcframework` and `StorytellerLottie.xcframework` to the app target and select **Embed & Sign** for both.\n- When using a Storyteller Ads extension, use compatible versions of the core SDK and extension and import the extension product separately.\n\nThe expected result is that the app imports, links, and launches with the SDK before `initialize(...)` runs. If a documented clean installation fails in a minimal app, capture the package resolution or linker error and [get help from Storyteller](#get-help-from-storyteller). See [Quickstart installation](Quickstart.md#install-the-sdk) for the complete setup.\n\n### Initialization Fails\n\n`Storyteller.shared.initialize(...)` is `async throws`. The clearest integration sequence is to await it and retain the original error before loading content:\n\n1. Confirm the API key identifies the intended tenant and environment.\n1. Confirm the current device can reach the required network services and that the app's network policy is not blocking the request.\n1. Inspect the error and `Storyteller.shared.isInitialized` after the call finishes.\n\nInitialization can throw `StorytellerError.networkError(Error)` for an unsuccessful server response. It can also surface an underlying URL, transport, or response-decoding error, so keep a general error path. After a thrown initialization call, `isInitialized` remains `false`; after success, it is `true`.\n\nIf the same key initializes in the intended environment but one component fails later, initialization is no longer the failing boundary. Continue with [Content Fails to Load](#content-fails-to-load).\n\n## Content Is Blank, Missing, or Still Loading\n\nObserve the component's load callback before changing its layout or content identifiers:\n\n- Story and Clips lists report `success`, `error`, and `dataCount` through `StorytellerListViewDelegate` or `StorytellerListAction`.\n- Cards report a `Result<Void, Error>` through their action or delegate.\n- Embedded Clips report `success` and `error` through their action or delegate.\n\nSee [Storyteller Delegates](StorytellerDelegate.md#storytellerlistviewdelegate), [Storyteller Cards](Cards.md), and [Embedded Clips](EmbeddedClips.md) for the callback used by each surface.\n\n### Content Fails to Load\n\nAn error or `success == false` means the request did not produce a successful content result. It is not the same as a successful response containing no available items.\n\n1. Record the returned error and whether initialization had already succeeded.\n1. Capture Storyteller SDK logs through [`StorytellerDelegate.log(message:)`](StorytellerDelegate.md#log) on the strongly retained object assigned to `Storyteller.shared.delegate`. Reproduce the failure and look for the request URL, transport error, or response-parsing error that corresponds to the callback. SDK request URLs can include a hashed user ID when `enableRemoteViewingStore` is enabled, even when personalization is disabled; they can also include custom-attribute values when personalization is enabled. Redact those values before forwarding logs to a third-party service or sharing them.\n1. Confirm the category or collection identifier was copied from the same Storyteller tenant as the API key. When the app has separate development and production configurations, check that the key and identifier came from the same configuration.\n\nIf initialization also fails, return to [Initialization Fails](#initialization-fails). If initialization succeeds and the same request repeatedly fails with valid inputs and connectivity, retain the error and request details and [get help from Storyteller](#get-help-from-storyteller).\n\n### Content Is Missing or Empty\n\nFor lists, `success == true`, no error, and `dataCount == 0` establishes that the request completed but returned no items available to this component and targeting context.\n\nCheck:\n\n- The category or collection identifier is the intended one and belongs to the same tenant and environment as the API key.\n- The content is assigned to that category or collection and is published rather than draft.\n- The publication schedule has started and the content has not expired.\n- When availability depends on an external user ID or custom attributes, `Storyteller.shared.eventTrackingOptions.enablePersonalization` was enabled during initialization. When it is disabled, those targeting values are omitted from supported content requests. The standard `userId` query parameter on those requests also requires `enableRemoteViewingStore`; remote-viewing requests follow the separate contract described in [Privacy and Tracking](PrivacyAndTracking.md#remote-viewing-store).\n- The external user ID, custom attributes, locale, followed-category state, and other targeting inputs make the content available.\n- The selected component and configuration are intended for that content type.\n\nThe expected control is a known, currently available item for the same targeting inputs and environment. If that appears, the original result belongs to content or targeting configuration rather than layout. See [Working with Users](Users.md) for external IDs and targeting inputs, [Privacy and Tracking](PrivacyAndTracking.md) for initialization-time personalization settings, and [Storyteller List Views](StorytellerListViews.md) for list identifiers and configuration.\n\n### Content Loaded but Is Not Visible\n\nFor lists, `success == true` with `dataCount > 0` establishes that content loaded. Diagnose presentation next:\n\n- Give a UIKit row or grid valid Auto Layout constraints and a non-zero size.\n- Give a SwiftUI row an appropriate non-zero frame in its parent layout.\n- Confirm the component is in the visible view hierarchy and is not hidden by host-app state.\n- Check configuration that changes list visibility or sizing, such as `visibleTiles`, before forcing a fixed height.\n\nThe expected result is that the already loaded items become visible without changing the content identifier. Start from the working layouts in the [iOS Quickstart Guide](Quickstart.md#add-your-first-stories-row), then move to [Storyteller List Views](StorytellerListViews.md) for advanced layouts.\n\n### Loading Does Not Complete\n\nStory and Clips lists and Cards can defer a load requested before initialization. This is a safeguard, not the recommended integration sequence; await initialization explicitly. A deferred Story or Clips list reload continues only when its list identifier remains unchanged while it waits. Cards wait for initialization without that identifier check.\n\nIf the app requests a load but no completion arrives:\n\n1. Confirm `initialize(...)` eventually completed successfully. If an attempt throws, the deferred load remains pending and can continue after a later successful retry.\n1. For a Story or Clips list requested before initialization, confirm its identifier did not change while waiting. If it changed, the SDK abandons that deferred reload before `onDataLoadStarted()` or `onDataLoadComplete(...)`; after initialization succeeds and the deferred attempt unwinds, request a new reload with the current identifier.\n1. Do not issue another Story or Clips list reload while an earlier reload is in progress, including while it waits for initialization. The later call is ignored and does not produce its own `onDataLoadStarted()` or `onDataLoadComplete(...)`; wait for the earlier attempt to finish or unwind before retrying.\n1. Assign the delegate or action before starting the load and keep any weak delegate strongly referenced by app-owned state.\n1. Confirm the device remains online and use the SDK log callback and network diagnostics to inspect whether a request starts or stalls.\n1. Reduce the integration to one known content identifier and one component.\n\nThe expected result is a terminal success, empty, or failure callback. If no request starts, focus on app lifecycle and configuration. If a request starts but never finishes under repeatable network conditions, capture the last observed request boundary and [get help from Storyteller](#get-help-from-storyteller).\n\n## Callbacks or Analytics Events Do Not Arrive\n\nFirst identify the callback layer:\n\n| Callback layer | Primary check | Expected observation |\n| --- | --- | --- |\n| List, Card, or Embedded Clips loading | Assign the surface's delegate or action before loading; strongly retain weak delegates. | A load-start or completion result arrives independently of analytics settings. |\n| Player interaction and app navigation | Assign and strongly retain `Storyteller.shared.delegate`; the SDK property is weak. | The implemented callback arrives when its corresponding interaction occurs. |\n| Analytics through `onUserActivityOccurred` | Set tracking options during initialization and keep `enableUserActivityTracking` enabled. | Supported non-Ad interactions produce user-activity events. |\n| Ad analytics through `onUserActivityOccurred` | Also keep `enableAdTracking` enabled and reach an actual Ad opportunity or interaction. | Ad events arrive only when an Ad lifecycle event occurs. |\n\n`eventTrackingOptions` can only be selected during initialization. Reinitialize the SDK to change them. `enableUserActivityTracking` gates `onUserActivityOccurred`; disabling `enableAdTracking` additionally suppresses Ad-related analytics and omits default KVPs and `customKvps` from supported Google Ad requests.\n\nTracking options govern analytics delivery; they do not determine whether a content request or provider Ad request is made. Use the callback belonging to the affected surface before changing privacy settings.\n\nSee [Integrate Analytics](AnalyticsIntegration.md), [Privacy and Tracking](PrivacyAndTracking.md), the [Analytics Event Reference](Analytics.md), and [Storyteller Delegates](StorytellerDelegate.md) for the owning contracts.\n\n## Appearance or Configuration Does Not Change\n\nCheck which configuration should win:\n\n1. `Storyteller.shared.theme` is the global fallback theme. Set it early in the app lifecycle.\n1. A theme passed in a list configuration applies to that component instead of the global fallback.\n1. For properties supported by remote appearance settings, a feed- or collection-specific remote value overrides the tenant/global remote value, which overrides the host-supplied light or dark theme. Confirm the active CMS values before treating a host customization as an SDK gap.\n1. The list's `uiStyle` selects the light or dark theme branch. For a universal customization, configure `theme.light`, copy it with `theme.dark = theme.light`, then apply any dark-specific overrides.\n1. After changing list configuration, reconfigure and reload the UIKit view, or update and reload the SwiftUI model, as described in [Storyteller List Views](StorytellerListViews.md).\n1. Test one deliberately visible property on a basic component before combining multiple overrides.\n\nIf the change works on the basic component but not the production surface, compare per-component overrides, the active appearance branch, and configuration timing. If the documented property never affects its supported surface in a minimal integration, record the property, surface, and active theme branch as a possible SDK gap.\n\nUse [Custom Themes](Themes.md) for supported properties and their scopes. The troubleshooting route does not imply that every visual detail is customizable.\n\n## Ads Do Not Appear\n\nAds cross several ownership boundaries. Establish the configured source before debugging the provider:\n\n- Storyteller First Party Ads are managed through the Storyteller CMS and require no additional provider integration in the app.\n- GAM, AdMob, VAST, and GAM VAST Ads require the matching Storyteller extension and provider setup.\n\nThen identify the exact placement:\n\n- For fullscreen Story or Clip Ads, confirm the tenant's Ads strategy enables the content type and placement you are testing. For a Clips opening pre-roll, the presentation must also opt in with `StorytellerClipsAdConfiguration(preRollEnabled: true)`.\n- For a Clips bottom banner, confirm the Storyteller feed response enables the placement with `showBottomBannerAd == true`, the presentation opts in with `StorytellerClipsAdConfiguration(bottomBannerEnabled: true)`, and the active Ads module supports bottom banners. This placement is separate from the fullscreen and opening pre-roll strategy.\n\nFollow the branch for the configured source and placement:\n\n| Configured source | Supported placement and next check |\n| --- | --- |\n| Storyteller First Party Ads | Confirm an eligible Ad is available in the CMS for a fullscreen placement, then observe whether the SDK displays it. First Party Ads do not support Clips bottom banners. |\n| GAM or AdMob Ads | Confirm the app installed and configured the matching extension, then use the extension and provider diagnostics to establish whether the placement produced a provider request. A Clips bottom banner additionally requires `bottomBannerAdUnit`. |\n| VAST or GAM VAST Ads | Confirm the app installed and configured the matching extension, then inspect its diagnostics for a fullscreen placement. These modules do not support Clips bottom banners. |\n\nContinue from the last source-specific observation:\n\n1. If a provider request starts, inspect its diagnostics or error for no-fill, trafficking, creative, consent, bidder, or network failures.\n1. If the CMS or provider makes an eligible Ad available but Storyteller does not display it, capture the Ad source, returned format when applicable, and subsequent SDK events or logs.\n\nInterpret the last successful boundary:\n\n| Last observation | Next owner to check |\n| --- | --- |\n| No eligible First Party Ad is available in the CMS for a fullscreen placement | Tenant Ads strategy, cadence, First Party Ad configuration, or content state |\n| No Clips bottom-banner request | The feed response's `showBottomBannerAd` value, the presentation's `bottomBannerEnabled` opt-in, module placement support, or `bottomBannerAdUnit` |\n| No provider request after an eligible extension-backed placement | Tenant Ads strategy, cadence, configured source, extension setup, placement support, or SDK routing |\n| Provider request returns no-fill or an error | External provider setup, inventory, trafficking, consent, bidder, or connectivity |\n| CMS or provider makes an Ad available but it cannot be mapped or rendered | Returned creative/format compatibility or SDK integration/rendering |\n\nTracking settings are not proof that an Ad was requested or rendered. They control analytics delivery and, for supported provider requests, which targeting values are sent; disabling them can therefore affect a line item's ability to match without directly enabling or disabling an Ad placement.\n\nSee [Ads](Ads.md) for provider setup, placement support, and diagnostics. Provider-specific rewrites and live trafficking checks remain in the provider's own integration workflow.\n\n## Get Help from Storyteller\n\nWhen the documented next check does not resolve the problem, share as much of the following information as is available through your normal Storyteller support channel:\n\n- Storyteller SDK version from `Storyteller.shared.version` and the versions of any Storyteller extension modules.\n- Installation method, Xcode version, iOS version, and device or simulator model.\n- Affected surface: UIKit or SwiftUI, Stories, Clips, Cards, Embedded Clips, Player, or Ad placement.\n- Minimal steps, expected result, actual result, and reproducibility.\n- The last successful boundary and the complete callback result or error.\n- Sanitized SDK logs captured through [`StorytellerDelegate.log(message:)`](StorytellerDelegate.md#log), network or provider logs, and a screenshot or short recording when presentation is relevant. Redact hashed user IDs, custom-attribute values, and any other personal data from request URLs before forwarding the logs to a third-party service or sharing them.\n- API key, tenant/environment name, and relevant content or Ad identifiers.\n- Whether the same setup succeeds with a known control item, minimal component, or Showcase-equivalent integration.\n\nAn SDK defect is more likely when the same supported configuration fails consistently with valid inputs, successful initialization, known available content or a valid provider response, and a minimal reproduction. Identifying the last successful callback or request boundary helps Storyteller distinguish that from an app, tenant, content, or provider problem.\n\nStoryteller API keys and content identifiers can be shared with Storyteller because they identify the tenant and content. SDK request URLs can contain hashed user IDs when `enableRemoteViewingStore` is enabled, even when personalization is disabled; they can also contain custom-attribute values when personalization is enabled. Redact those values and any other personal data before forwarding or sharing the logs. Do not include access tokens or unredacted provider credentials.\n", "copy_markdown_include_header": false, "base_path": "", "ai_dir": "ai", "missing_payload_behavior": "empty"}