StorytellerDelegate is the global route for events and integration hooks associated with Story and Clip Players. It inherits from StorytellerModule, described in StorytellerModule. Component callbacks are separate and do not replace the global delegate.
This protocol applies to both UIKit and SwiftUI apps. Assign one app-owned instance to Storyteller.shared.delegate and retain it strongly because the SDK property is weak. Implement only the optional callbacks your integration needs.
The onUserActivityOccurred(type: StorytellerUserActivity.EventType, data: StorytellerUserActivityData) method is called when an analytics event is triggered within the SDK. This allows the integrating app to observe and potentially forward these events to their own analytics systems. Follow Integrate Analytics for setup and verification, then use the Analytics Event Reference for event types and data.
The getAd(for adRequestInfo: StorytellerAdRequestInfo) async throws -> StorytellerAd method is called when the tenant is configured in the CMS to request full-screen ads from the integrating app. The app should fetch ad data asynchronously and return it directly to the SDK, or throw an error if no ad is available. See the Ads page for more details.
getBottomBannerAd(for adRequestInfo: StorytellerAdRequestInfo, maxHeight: CGFloat) async throws -> StorytellerAd method is similar to getAd, but is called when the tenant is configured in the CMS to request bottom banner ads (displayed at the bottom of clips) from the integrating app. The maxHeight parameter indicates the maximum allowed height for the banner based on the current layout constraints. See the Ads page for more details.
The userNavigatedToApp(url: String) method is called when a user presses an action button on a page which should direct the user to a specific place within the integrating app. More information on In App links Navigating to App. For more information on deep linking, see the dedicated Deep linking page.
The onShareButtonTapped(text: String, title: String, url: String) method is called when Storyteller.shared.useCustomShareHandling is set to true and a user taps the Share button in a Story or Clip. The SDK pauses the current Story or Clip, skips presenting the iOS share sheet, and forwards the same payload it would normally share so your app can present its own share flow.
When Storyteller.shared.useCustomShareHandling remains false (the default), the SDK continues to present the native iOS share sheet and this callback is not invoked.
When your custom share UI is dismissed, call Storyteller.shared.resumePlayer() to resume Storyteller playback.
This method allows you to configure the WebView with custom settings or actions when the Storyteller SDK is about to display a WebView. This method is called before displaying WebView on the screen.
It receives a configuration object which is a collection of properties that you use to initialize a web view.
The method categoryFollowActionTaken(category: StorytellerCategory, isFollowing: Bool) is invoked when a user follows or unfollows a category of clips (the category can represent a player, a team etc.) from within the SDK's UI.
The callback reports the affected category and whether it is now followed:
category - An object representing the clip category
isFollowing - A boolean value indicating whether the user is following or unfollowing the specified category
The log(message:) method receives Storyteller SDK error and informational log messages. Implement it on the app-owned object assigned to Storyteller.shared.delegate when you need to capture diagnostics in debug or release builds. The SDK holds the delegate weakly, so keep that object strongly retained. Failed-request messages can include full request URLs containing a hashed user ID when 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. Storyteller API keys can remain intact when sharing logs directly with Storyteller support.
The method onPlayerPresented() is invoked when a story or clip player is presented on screen. This can be useful for pausing background audio, videos or animations in your app while the player is visible.
The method onPlayerDismissed() is invoked when a story or clip player is dismissed from screen. This can be useful for resuming background audio, videos or animations in your app that were paused when the player was presented.
The method viewController(for category: StorytellerCategory) -> UIViewController? is invoked when the user taps on the category icon inside a clip or interactively swipes to the left. You can provide a custom view controller to push to. This method is optional, and if an implementation is not provided, the SDK will push a UIViewController with a Story Row and Clip Grid based on the Category.
This delegate method receives the following parameter:
category - An object representing the clip category
Every StorytellerDelegate method has a default implementation, including the Ad methods inherited from StorytellerModule. Implement only the callbacks your app needs. This minimal analytics delegate is valid without placeholder return values:
Keep the delegate strongly referenced, as shown by StorytellerIntegration; Storyteller.shared.delegate is weak. Implement getAd or getBottomBannerAd only when your tenant requests host-supplied Ads, and return a real StorytellerAd or throw an error as described in Ads.
WebKit types are not re-exported by StorytellerSDK. A delegate that customizes Storyteller WebViews must import WebKit explicitly:
Storyteller invokes configureWebView while constructing UI on the main actor. The released protocol requirement predates WebKit's strict-concurrency annotations, so MainActor.assumeIsolated keeps host code warning-free on newer toolchains while preserving compatibility with the released SDK.
StorytellerListViewDelegate is the UIKit callback interface for StorytellerRowView and StorytellerGridView subclasses. SwiftUI list wrappers report the same event values through StorytellerListActionCallback; choose the corresponding wiring example below.
UIKit calls onTileTapped(type:) and SwiftUI emits .onTileTapped(type:) when a user taps a tile inside a row or grid. This happens before the Player is opened.
Property
Description
type
A StorytellerTileType enum that contains tile information. Can be either .story(storyId: String, categories: [StorytellerCategoryDetail]) or .clip(clipId: String, collectionId: String, categories: [StorytellerCategoryDetail])
Note: When theme.lists.enablePlayerOpen is set to false, the SDK will not automatically open the player and you should handle your custom tile interaction logic via this callback. For lists in SDK‑owned screens (Storyteller Home, Followable Categories, and Search), the SDK always opens the player when a tile is tapped, regardless of theme.lists.enablePlayerOpen.
Example:
// Assuming `theme.lists.enablePlayerOpen` is set to `false`funconTileTapped(type:StorytellerTileType){switchtype{case.story(letstoryId,letcategories):letcategoryIds=categories.map(\.id)// Handle story tile tapcase.clip(letclipId,letcollectionId,letcategories):letcategoryIds=categories.map(\.id)// Handle clip tile tap@unknowndefault:break}}
By using the callback function onDataLoadComplete and the data it provides, you can handle the current state of the StorytellerRowView appropriately in your app.
Note: dataCount is the total number of Stories in the existing StorytellerRowView at any given time
Example:
funconDataLoadComplete(success:Bool,error:Error?,dataCount:Int){ifsuccess{// stories data has been loaded successfully// dataCount is the current total number of content, including newly added/removed data}elseifletnewError=error{// an error has occurred, use the unwrapped value `newError`}}
Another example:
letstorytellerRowView=StorytellerStoriesRowView()funconDataLoadComplete(success:Bool,error:Error?,dataCount:Int){iflet_=error,dataCount==0{// content have failed to load with error and there is no data to show// you may wish to hide the `StorytellerRowView` instance herestorytellerRowView.isHidden=true// Example: storytellerRowViewHeightConstraint.constant = 0}}
Example implementation of StorytellerListViewDelegate#
Implement StorytellerListViewDelegate:
classDelegateObject:StorytellerListViewDelegate{funconDataLoadStarted(){// Action on start of data network requests}funconDataLoadComplete(success:Bool,error:Error?,dataCount:Int){// Action on completion of data network requests}funconTileTapped(type:StorytellerTileType){// Action when a tile is tapped}funconPlayerDismissed(){// Action on dismissal of player}}
Retain the delegate strongly, assign it to the UIKit view, and then load the content:
Assign the delegate before reloadData() or the delegate will not receive the initial loading callbacks.
Pass an action closure to the SwiftUI list wrapper and switch over StorytellerListAction:
importStorytellerSDKimportSwiftUI@available(iOS14.0,*)structStoriesRowWithActions:View{@Stateprivatevarmodel=StorytellerStoriesListModel(configuration:StorytellerStoriesListConfiguration(categories:["sports"]))varbody:someView{StorytellerStoriesRow(model:model){actioninswitchaction{case.onDataLoadStarted:print("Storyteller list started loading")case.onDataLoadComplete(letsuccess,leterror,letdataCount):print("Loaded \(dataCount) items; success: \(success); error: \(error?.localizedDescription??"none")")case.onTileTapped(lettype):print("Tapped Storyteller tile: \(type)")case.onPlayerDismissed:print("Storyteller Player dismissed")@unknowndefault:break}}}}
The wrapper owns the internal UIKit delegate bridge. Your SwiftUI code should consume the action closure rather than constructing a StorytellerListViewDelegate.
{"slug": "storyteller-delegate", "page_title": "Handle Delegates and Callbacks", "page_url": "StorytellerDelegate/", "canonical_url": "/ios/StorytellerDelegate/", "markdown": "# Implementing Storyteller Delegate Callbacks\n\nStoryteller has a framework-independent global delegate and framework-specific component callback routes:\n\n| What you need to observe | UIKit | SwiftUI |\n| --- | --- | --- |\n| Player lifecycle, analytics, Ads, sharing, logging, or in-app navigation | `StorytellerDelegate` assigned to `Storyteller.shared.delegate` | The same `StorytellerDelegate` |\n| Story or Clip row/grid loading, taps, or Player dismissal | `StorytellerListViewDelegate` assigned to the UIKit view | `StorytellerListActionCallback` passed to the SwiftUI wrapper |\n| Embedded Clips loading or top-level back navigation | `StorytellerClipsViewControllerDelegate` | `StorytellerClipsView` action closure; see [Embedded Clips](EmbeddedClips.md#delegate) |\n\n`StorytellerDelegate` is the global route for events and integration hooks associated with Story and Clip Players. It inherits from `StorytellerModule`, described in [StorytellerModule](StorytellerModule.md). Component callbacks are separate and do not replace the global delegate.\n\nFor a full implementation, see the Showcase delegate in [`StorytellerInstanceDelegate`](https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.1/main/ShowcaseApp/Storyteller/StorytellerInstanceDelegate.swift#L11) and the analytics forwarding in [`StorytellerTrackingDelegate`](https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.1/main/ShowcaseApp/Analytics/StorytellerTrackingDelegate.swift#L10).\n\nIf an expected callback does not arrive, use the shared [callback and analytics troubleshooting route](Troubleshooting.md#callbacks-or-analytics-events-do-not-arrive) to distinguish load callbacks, interaction callbacks, and analytics gates.\n\n## StorytellerDelegate\n\nThis protocol applies to both UIKit and SwiftUI apps. Assign one app-owned instance to `Storyteller.shared.delegate` and retain it strongly because the SDK property is weak. Implement only the optional callbacks your integration needs.\n\n### onUserActivityOccurred\n\nThe `onUserActivityOccurred(type: StorytellerUserActivity.EventType, data: StorytellerUserActivityData)` method is called when an analytics event is triggered within the SDK. This allows the integrating app to observe and potentially forward these events to their own analytics systems. Follow [Integrate Analytics](AnalyticsIntegration.md) for setup and verification, then use the [Analytics Event Reference](Analytics.md) for event types and data.\n\n### getAd\n\nThe `getAd(for adRequestInfo: StorytellerAdRequestInfo) async throws -> StorytellerAd` method is called when the tenant is configured in the CMS to request full-screen ads from the integrating app. The app should fetch ad data asynchronously and return it directly to the SDK, or throw an error if no ad is available. See the [Ads](Ads.md) page for more details.\n\n### getBottomBannerAd\n\n`getBottomBannerAd(for adRequestInfo: StorytellerAdRequestInfo, maxHeight: CGFloat) async throws -> StorytellerAd` method is similar to `getAd`, but is called when the tenant is configured in the CMS to request bottom banner ads (displayed at the bottom of clips) from the integrating app. The `maxHeight` parameter indicates the maximum allowed height for the banner based on the current layout constraints. See the [Ads](Ads.md) page for more details.\n\n### userNavigatedToApp\n\nThe `userNavigatedToApp(url: String)` method is called when a user presses an action button on a page which should direct the user to a specific place within the integrating app. More information on `In App` links [Navigating to App](NavigatingToApp.md). For more information on deep linking, see the dedicated [Deep linking](Deeplinking.md) page.\n\n### onShareButtonTapped\n\nThe `onShareButtonTapped(text: String, title: String, url: String)` method is called when `Storyteller.shared.useCustomShareHandling` is set to `true` and a user taps the Share button in a Story or Clip. The SDK pauses the current Story or Clip, skips presenting the iOS share sheet, and forwards the same payload it would normally share so your app can present its own share flow.\n\nWhen `Storyteller.shared.useCustomShareHandling` remains `false` (the default), the SDK continues to present the native iOS share sheet and this callback is not invoked.\n\nWhen your custom share UI is dismissed, call `Storyteller.shared.resumePlayer()` to resume Storyteller playback.\n\n### configureWebView\n\nThis method allows you to configure the WebView with custom settings or actions when the Storyteller SDK is about to display a WebView. This method is called before displaying WebView on the screen.\n\nIt receives a `configuration` object which is a collection of properties that you use to initialize a web view.\n\n> Note: `configureWebView` is available only on iOS.\n\n### categoryFollowActionTaken\n\nThe method `categoryFollowActionTaken(category: StorytellerCategory, isFollowing: Bool)` is invoked when a user follows or unfollows a category of clips (the category can represent a player, a team etc.) from within the SDK's UI.\n{: #categoryfollowactiontakencategory-storytellersdkcategory-isfollowing-bool }\n\nThe callback reports the affected category and whether it is now followed:\n{: #categoryfollowactiontakencategory-storytellercategory-isfollowing-bool }\n\n- `category` - An object representing the clip category\n- `isFollowing` - A boolean value indicating whether the user is following or unfollowing the specified category\n\n**Note:** This method is only called when your tenant is setup in [App-Managed Following mode](Users.md#app-managed-following).\n\n### log\n\nThe `log(message:)` method receives Storyteller SDK error and informational log messages. Implement it on the app-owned object assigned to `Storyteller.shared.delegate` when you need to capture diagnostics in debug or release builds. The SDK holds the delegate weakly, so keep that object strongly retained. Failed-request messages can include full request URLs containing a hashed user ID when `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. Storyteller API keys can remain intact when sharing logs directly with Storyteller support.\n\n### onPlayerPresented\n\nThe method `onPlayerPresented()` is invoked when a story or clip player is presented on screen. This can be useful for pausing background audio, videos or animations in your app while the player is visible.\n\n### onPlayerDismissed\n\nThe method `onPlayerDismissed()` is invoked when a story or clip player is dismissed from screen. This can be useful for resuming background audio, videos or animations in your app that were paused when the player was presented.\n\n### viewController(for: StorytellerCategory)\n\nThe method `viewController(for category: StorytellerCategory) -> UIViewController?` is invoked when the user taps on the category icon inside a clip or interactively swipes to the left. You can provide a custom view controller to push to. This method is optional, and if an implementation is not provided, the SDK will push a `UIViewController` with a Story Row and Clip Grid based on the Category.\n\nThis delegate method receives the following parameter:\n\n- `category` - An object representing the clip category\n\n### Minimal `StorytellerDelegate` implementation\n\nEvery `StorytellerDelegate` method has a default implementation, including the Ad methods inherited from `StorytellerModule`. Implement only the callbacks your app needs. This minimal analytics delegate is valid without placeholder return values:\n\n<!-- storyteller-swift-example: id=storytellerdelegate-01 target=sdk-ios context=declarations -->\n\n```swift\nimport StorytellerSDK\n\nfinal class AnalyticsDelegate: StorytellerDelegate {\n func onUserActivityOccurred(\n type: StorytellerUserActivity.EventType,\n data: StorytellerUserActivityData\n ) {\n print(\"Storyteller event: \\(type), context: \\(data.context ?? [:])\")\n }\n}\n\nfinal class StorytellerIntegration {\n private let delegate = AnalyticsDelegate()\n\n func configure() {\n Storyteller.shared.delegate = delegate\n }\n}\n```\n\nKeep the delegate strongly referenced, as shown by `StorytellerIntegration`; `Storyteller.shared.delegate` is weak. Implement `getAd` or `getBottomBannerAd` only when your tenant requests host-supplied Ads, and return a real `StorytellerAd` or throw an error as described in [Ads](Ads.md).\n\nWebKit types are not re-exported by StorytellerSDK. A delegate that customizes Storyteller WebViews must import `WebKit` explicitly:\n\n<!-- storyteller-swift-example: id=storytellerdelegate-02 target=sdk-ios context=declarations -->\n\n```swift\nimport StorytellerSDK\nimport WebKit\n\nfinal class WebViewDelegate: StorytellerDelegate {\n func configureWebView(configuration: inout WKWebViewConfiguration) {\n MainActor.assumeIsolated {\n let script = WKUserScript(\n source: \"document.body.style.backgroundColor = 'red';\",\n injectionTime: .atDocumentEnd,\n forMainFrameOnly: true\n )\n configuration.userContentController.addUserScript(script)\n }\n }\n}\n```\n\nStoryteller invokes `configureWebView` while constructing UI on the main actor. The released protocol requirement predates WebKit's strict-concurrency annotations, so `MainActor.assumeIsolated` keeps host code warning-free on newer toolchains while preserving compatibility with the released SDK.\n\n## StorytellerListViewDelegate\n\n`StorytellerListViewDelegate` is the UIKit callback interface for `StorytellerRowView` and `StorytellerGridView` subclasses. SwiftUI list wrappers report the same event values through `StorytellerListActionCallback`; choose the corresponding wiring example below.\n\n### onDataLoadStarted\n\nUIKit calls `onDataLoadStarted()` and SwiftUI emits `.onDataLoadStarted` when a list request begins.\n\n### onDataLoadComplete\n\nUIKit calls `onDataLoadComplete(success:error:dataCount:)` and SwiftUI emits `.onDataLoadComplete(success:error:dataCount:)` when the request finishes.\n\n| Property | Description |\n| --------------------- | ------------------------------------------------------- |\n| `success` | This confirms whether or not the request was successful |\n| `error` | The HTTP error if the request was not successful |\n| `dataCount` | The number of Stories loaded |\n\n### onTileTapped\n\nUIKit calls `onTileTapped(type:)` and SwiftUI emits `.onTileTapped(type:)` when a user taps a tile inside a row or grid. This happens before the Player is opened.\n\n| Property | Description |\n| -------- | ----------- |\n| `type` | A `StorytellerTileType` enum that contains tile information. Can be either `.story(storyId: String, categories: [StorytellerCategoryDetail])` or `.clip(clipId: String, collectionId: String, categories: [StorytellerCategoryDetail])` |\n\n> Note: When `theme.lists.enablePlayerOpen` is set to `false`, the SDK will not automatically open the player and you should handle your custom tile interaction logic via this callback. For lists in SDK\u2011owned screens (Storyteller Home, Followable Categories, and Search), the SDK always opens the player when a tile is tapped, regardless of `theme.lists.enablePlayerOpen`.\n\nExample:\n\n<!-- storyteller-swift-example: id=storytellerdelegate-03 target=sdk-ios context=statements -->\n\n```swift\n// Assuming `theme.lists.enablePlayerOpen` is set to `false`\nfunc onTileTapped(type: StorytellerTileType) {\n switch type {\n case .story(let storyId, let categories):\n let categoryIds = categories.map(\\.id)\n // Handle story tile tap\n case .clip(let clipId, let collectionId, let categories):\n let categoryIds = categories.map(\\.id)\n // Handle clip tile tap\n @unknown default:\n break\n }\n}\n```\n\n### `onPlayerDismissed` {#onplayerdismissed_1}\n\nUIKit calls `onPlayerDismissed()` and SwiftUI emits `.onPlayerDismissed` when a Player opened from the list is dismissed.\n\n### Error Handling\n\nBy using the callback function `onDataLoadComplete` and the data it provides, you can handle the current state of the `StorytellerRowView` appropriately in your app.\n\n> Note: `dataCount` is the total number of Stories in the existing `StorytellerRowView` at any given time\n\nExample:\n\n<!-- storyteller-swift-example: id=storytellerdelegate-04 target=sdk-ios context=statements -->\n\n```swift\nfunc onDataLoadComplete(success: Bool, error: Error?, dataCount: Int) {\n if success {\n // stories data has been loaded successfully\n // dataCount is the current total number of content, including newly added/removed data\n } else if let newError = error {\n // an error has occurred, use the unwrapped value `newError`\n }\n}\n```\n\nAnother example:\n\n<!-- storyteller-swift-example: id=storytellerdelegate-05 target=sdk-ios context=statements -->\n\n```swift\nlet storytellerRowView = StorytellerStoriesRowView()\n\nfunc onDataLoadComplete(success: Bool, error: Error?, dataCount: Int) {\n if let _ = error, dataCount == 0 {\n // content have failed to load with error and there is no data to show\n // you may wish to hide the `StorytellerRowView` instance here\n storytellerRowView.isHidden = true\n // Example: storytellerRowViewHeightConstraint.constant = 0\n }\n}\n```\n\n### Wire List Callbacks\n\nChoose the callback route for your framework:\n\n=== \"UIKit\"\n\n #### Example implementation of StorytellerListViewDelegate\n\n Implement `StorytellerListViewDelegate`:\n\n <!-- storyteller-swift-example: id=storytellerdelegate-06 target=sdk-ios context=declarations -->\n\n ```swift\n class DelegateObject : StorytellerListViewDelegate {\n\n func onDataLoadStarted() {\n // Action on start of data network requests\n }\n\n func onDataLoadComplete(success: Bool, error: Error?, dataCount: Int) {\n // Action on completion of data network requests\n }\n\n func onTileTapped(type: StorytellerTileType) {\n // Action when a tile is tapped\n }\n\n func onPlayerDismissed() {\n // Action on dismissal of player\n }\n }\n ```\n\n Retain the delegate strongly, assign it to the UIKit view, and then load the content:\n\n <!-- storyteller-swift-example: id=storytellerdelegate-07 target=sdk-ios context=declarations -->\n\n ```swift\n import StorytellerSDK\n import UIKit\n\n final class StoriesListViewController: UIViewController {\n private let storytellerStoriesRow = StorytellerStoriesRowView()\n private let delegate = DelegateObject()\n\n override func viewDidLoad() {\n super.viewDidLoad()\n\n storytellerStoriesRow.delegate = delegate\n storytellerStoriesRow.reloadData()\n }\n\n private final class DelegateObject: StorytellerListViewDelegate {}\n }\n ```\n\n Assign the delegate before `reloadData()` or the delegate will not receive the initial loading callbacks.\n\n=== \"SwiftUI\"\n\n Pass an action closure to the SwiftUI list wrapper and switch over `StorytellerListAction`:\n\n <!-- storyteller-swift-example: id=storytellerdelegate-swiftui-list-actions target=sdk-ios context=declarations -->\n\n ```swift\n import StorytellerSDK\n import SwiftUI\n\n @available(iOS 14.0, *)\n struct StoriesRowWithActions: View {\n @State private var model = StorytellerStoriesListModel(\n configuration: StorytellerStoriesListConfiguration(categories: [\"sports\"])\n )\n\n var body: some View {\n StorytellerStoriesRow(model: model) { action in\n switch action {\n case .onDataLoadStarted:\n print(\"Storyteller list started loading\")\n case .onDataLoadComplete(let success, let error, let dataCount):\n print(\"Loaded \\(dataCount) items; success: \\(success); error: \\(error?.localizedDescription ?? \"none\")\")\n case .onTileTapped(let type):\n print(\"Tapped Storyteller tile: \\(type)\")\n case .onPlayerDismissed:\n print(\"Storyteller Player dismissed\")\n @unknown default:\n break\n }\n }\n }\n }\n ```\n\n The wrapper owns the internal UIKit delegate bridge. Your SwiftUI code should consume the action closure rather than constructing a `StorytellerListViewDelegate`.\n\nSee [Storyteller List Views](StorytellerListViews.md#configure-example) for complete list configuration and reload examples.\n", "copy_markdown_include_header": false, "base_path": "", "ai_dir": "ai", "missing_payload_behavior": "empty"}