Implementing Storyteller Delegate Callbacks#
Storyteller has a framework-independent global delegate and framework-specific component callback routes:
| What you need to observe | UIKit | SwiftUI |
|---|---|---|
| Player lifecycle, analytics, Ads, sharing, logging, or in-app navigation | StorytellerDelegate assigned to Storyteller.shared.delegate |
The same StorytellerDelegate |
| Story or Clip row/grid loading, taps, or Player dismissal | StorytellerListViewDelegate assigned to the UIKit view |
StorytellerListActionCallback passed to the SwiftUI wrapper |
| Embedded Clips loading or top-level back navigation | StorytellerClipsViewControllerDelegate |
StorytellerClipsView action closure; see Embedded Clips |
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.
For a full implementation, see the Showcase delegate in StorytellerInstanceDelegate and the analytics forwarding in StorytellerTrackingDelegate.
If an expected callback does not arrive, use the shared callback and analytics troubleshooting route to distinguish load callbacks, interaction callbacks, and analytics gates.
StorytellerDelegate#
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.
onUserActivityOccurred#
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.
getAd#
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#
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.
userNavigatedToApp#
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.
onShareButtonTapped#
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.
configureWebView#
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.
Note:
configureWebViewis available only on iOS.
categoryFollowActionTaken#
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 categoryisFollowing- A boolean value indicating whether the user is following or unfollowing the specified category
Note: This method is only called when your tenant is setup in App-Managed Following mode.
log#
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.
onPlayerPresented#
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.
onPlayerDismissed#
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.
viewController(for: StorytellerCategory)#
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
Minimal StorytellerDelegate implementation#
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:
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
}
}
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:
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)
}
}
}
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#
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.
onDataLoadStarted#
UIKit calls onDataLoadStarted() and SwiftUI emits .onDataLoadStarted when a list request begins.
onDataLoadComplete#
UIKit calls onDataLoadComplete(success:error:dataCount:) and SwiftUI emits .onDataLoadComplete(success:error:dataCount:) when the request finishes.
| Property | Description |
|---|---|
success |
This confirms whether or not the request was successful |
error |
The HTTP error if the request was not successful |
dataCount |
The number of Stories loaded |
onTileTapped#
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.enablePlayerOpenis set tofalse, 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 oftheme.lists.enablePlayerOpen.
Example:
// Assuming `theme.lists.enablePlayerOpen` is set to `false`
func onTileTapped(type: StorytellerTileType) {
switch type {
case .story(let storyId, let categories):
let categoryIds = categories.map(\.id)
// Handle story tile tap
case .clip(let clipId, let collectionId, let categories):
let categoryIds = categories.map(\.id)
// Handle clip tile tap
@unknown default:
break
}
}
onPlayerDismissed#
UIKit calls onPlayerDismissed() and SwiftUI emits .onPlayerDismissed when a Player opened from the list is dismissed.
Error Handling#
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:
dataCountis the total number of Stories in the existingStorytellerRowViewat any given time
Example:
func onDataLoadComplete(success: Bool, error: Error?, dataCount: Int) {
if success {
// stories data has been loaded successfully
// dataCount is the current total number of content, including newly added/removed data
} else if let newError = error {
// an error has occurred, use the unwrapped value `newError`
}
}
Another example:
let storytellerRowView = StorytellerStoriesRowView()
func onDataLoadComplete(success: Bool, error: Error?, dataCount: Int) {
if let _ = 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 here
storytellerRowView.isHidden = true
// Example: storytellerRowViewHeightConstraint.constant = 0
}
}
Wire List Callbacks#
Choose the callback route for your framework:
Example implementation of StorytellerListViewDelegate#
Implement StorytellerListViewDelegate:
class DelegateObject : StorytellerListViewDelegate {
func onDataLoadStarted() {
// Action on start of data network requests
}
func onDataLoadComplete(success: Bool, error: Error?, dataCount: Int) {
// Action on completion of data network requests
}
func onTileTapped(type: StorytellerTileType) {
// Action when a tile is tapped
}
func onPlayerDismissed() {
// Action on dismissal of player
}
}
Retain the delegate strongly, assign it to the UIKit view, and then load the content:
import StorytellerSDK
import UIKit
final class StoriesListViewController: UIViewController {
private let storytellerStoriesRow = StorytellerStoriesRowView()
private let delegate = DelegateObject()
override func viewDidLoad() {
super.viewDidLoad()
storytellerStoriesRow.delegate = delegate
storytellerStoriesRow.reloadData()
}
private final class DelegateObject: StorytellerListViewDelegate {}
}
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:
import StorytellerSDK
import SwiftUI
@available(iOS 14.0, *)
struct StoriesRowWithActions: View {
@State private var model = StorytellerStoriesListModel(
configuration: StorytellerStoriesListConfiguration(categories: ["sports"])
)
var body: some View {
StorytellerStoriesRow(model: model) { action in
switch action {
case .onDataLoadStarted:
print("Storyteller list started loading")
case .onDataLoadComplete(let success, let error, let dataCount):
print("Loaded \(dataCount) items; success: \(success); error: \(error?.localizedDescription ?? "none")")
case .onTileTapped(let type):
print("Tapped Storyteller tile: \(type)")
case .onPlayerDismissed:
print("Storyteller Player dismissed")
@unknown default:
break
}
}
}
}
The wrapper owns the internal UIKit delegate bridge. Your SwiftUI code should consume the action closure rather than constructing a StorytellerListViewDelegate.
See Storyteller List Views for complete list configuration and reload examples.