Troubleshooting Storyteller on iOS#
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.
Start with the Observable Result#
Record the first unexpected result before changing the integration:
| Observable result | Check next | Expected observation | Likely owner | Continue with |
|---|---|---|---|---|
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 |
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 |
A content load callback reports an error or success == false |
Inspect the returned error and capture related SDK logs through StorytellerDelegate.log(message:). |
The callback distinguishes a failed request from an empty successful response. | App integration, network, tenant, or SDK request path | Content fails to load |
A list load succeeds with 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 |
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 |
| A load is requested but no completion callback arrives | Confirm initialization completed successfully, the callback recipient is retained, any deferred list identifier remained unchanged, and the request is not blocked. | The load reaches a success, empty, or failure callback. | App lifecycle or configuration, network, or SDK request path | Loading does not complete |
| A delegate, action, or analytics callback does not arrive | Identify whether it is a data-load, interaction, or analytics callback, then check its delegate lifetime and tracking gate. | Another callback in the same layer establishes whether loading, interaction, or event delivery is affected. | App callback setup, privacy configuration, tenant Ads setup, or SDK event delivery | Callbacks or analytics events do not arrive |
| Appearance or configuration changes have no effect | Check global versus per-component configuration, the active light/dark theme, any supported remote override, and whether the component was reconfigured or reloaded where required. | A deliberately visible change appears after the active host and remote precedence layers are established. | App or tenant/CMS configuration, or an unsupported SDK customization | Appearance or configuration does not change |
| Ads do not appear | Establish the configured Ad source, whether an eligible placement was reached, and whether the matching extension or provider made a request. | The last observed boundary identifies Storyteller setup, provider delivery, or rendering as the next owner. | Tenant Ads strategy, app extension, external provider, inventory, or SDK rendering | Ads do not appear |
Do not use a visual symptom alone to assign the cause. Keep the callback result, error, and last successful boundary together.
Installation or Initialization Fails#
The SDK Does Not Build or Load#
Follow the instructions for the installation method you actually use:
- With Swift Package Manager, add the
StorytellerSDKproduct 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.xcframeworkandStorytellerLottie.xcframeworkto 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.
Initialization Fails#
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.isInitializedafter 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.
Content Is Blank, Missing, or Still Loading#
Observe the component's load callback before changing its layout or content identifiers:
- Story and Clips lists report
success,error, anddataCountthroughStorytellerListViewDelegateorStorytellerListAction. - Cards report a
Result<Void, Error>through their action or delegate. - Embedded Clips report
successanderrorthrough their action or delegate.
See Storyteller Delegates, Storyteller Cards, and Embedded Clips for the callback used by each surface.
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 toStoryteller.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 whenenableRemoteViewingStoreis 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.
Content Is Missing or Empty#
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.enablePersonalizationwas enabled during initialization. When it is disabled, those targeting values are omitted from supported content requests. The standarduserIdquery parameter on those requests also requiresenableRemoteViewingStore; 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.
Content Loaded but Is Not Visible#
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.
Loading Does Not Complete#
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()oronDataLoadComplete(...); 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()oronDataLoadComplete(...); 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.
Callbacks or Analytics Events Do Not Arrive#
First identify the callback layer:
| Callback layer | Primary check | Expected observation |
|---|---|---|
| List, Card, or Embedded Clips loading | Assign the surface's delegate or action before loading; strongly retain weak delegates. | A load-start or completion result arrives independently of analytics settings. |
| Player interaction and app navigation | Assign and strongly retain 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.
See Integrate Analytics, Privacy and Tracking, the Analytics Event Reference, and Storyteller Delegates for the owning contracts.
Appearance or Configuration Does Not Change#
Check which configuration should win:
Storyteller.shared.themeis 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
uiStyleselects the light or dark theme branch. For a universal customization, configuretheme.light, copy it withtheme.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 Do Not Appear#
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 withStorytellerClipsAdConfiguration(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 | Tenant Ads strategy, cadence, configured source, extension setup, placement support, or SDK routing |
| Provider request returns no-fill or an error | External provider setup, inventory, trafficking, consent, bidder, or connectivity |
| CMS or provider makes an Ad available but it cannot be mapped or rendered | Returned creative/format compatibility or SDK integration/rendering |
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.
Get Help from Storyteller#
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.versionand 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.