Deep linking#
To make sharing and direct content links work, first complete the shared domain and CMS setup, then implement the URL-receiving route for either UIKit or SwiftUI. Both frameworks use Storyteller.shared.isStorytellerDeepLink(url:) and Storyteller.shared.openDeepLink(url:) after the app receives a URL.
Understanding Link Types#
iOS apps support two types of deep links, each serving different purposes:
Universal Links (HTTPS URLs)#
- When to use: When it's unknown whether the user has your app installed (e.g., sharing on social media, email links)
- Format:
https://[tenant_name].shar.estori.es/... - Behavior: iOS will open your app if installed, otherwise opens the web browser
- Setup: Requires Associated Domains configuration
URL Scheme Links (Custom URLs)#
- When to use: When you're certain the user has your app installed (e.g., push notifications, in-app navigation)
- Format:
[tenant_name]stories://... - Behavior: Directly opens your app; shows an error if not installed
- Setup: Requires custom URL scheme registration
Important: Push notifications on iOS do not support Universal Links for directly opening apps. You must use URL Scheme Links in push notification payloads to ensure your app opens correctly.
Add Associated Domain to Your Project Settings in Xcode#
At first you need to add associated domain to your projects.
1. Go to your project settings in Xcode -> Signing & Capabilities

2. Press +Capability
3. Choose Associated Domains

4. Add the following domains:
applinks:[tenant_name].ope.nstori.esapplinks:[tenant_name].shar.estori.es

Add Bundle Identifier to Storyteller CMS#
After setting up an associated domain you need to add a bundle identifier to Storyteller CMS.
1. Log into Storyteller CMS
2. Go to Apps

3. Create a new iOS app or edit existing one

4. Fill out App ID
App ID has the form <Application Identifier Prefix>.<Bundle Identifier>
e.g. ABCDE12345.com.example.app

5. Press Save
Register a Custom URL Scheme for your app#
Our SDK supports deeplinking through custom URL schemes. Custom URL schemes allow your application to be launched in a specific context from a custom URL. This is essential for push notifications, as iOS does not support Universal Links from push notifications.
In order to use the custom URL scheme supported by our SDK, you need to register it with the following format: [TENANT_NAME]stories://, E.g. gosportsstories://.
You can follow the next steps to do so:
1. Go to Info tab in your Xcode project settings

2. Expand URL Types section and add a new URL Type entry. For the Identifier field, you should use a unique identifier, like your app's bundle identifier for example. In URL Schemes, enter [TENANT_NAME]stories, replacing [TENANT_NAME] with your respective Storyteller tenant name. The Role field is only used for macOS applications, and can be ignored on iOS and other platforms.

After following these steps, your app should be able to directly launch our SDK in a specific context from a URL with a custom scheme.
Push Notifications: URL scheme links are essential for opening your app from push notifications. See the Handling URL Scheme Links from Push Notifications section below for implementation details.
This feature can be used for example, to directly open a story or a clip with a deeplink url. To directly open a story, the SDK will handle deeplinks with the following format:
[TENANT_ID]stories://open/[STORYID]/[PAGEID]
Or to open a clip:
[TENANT_ID]stories://open/clip/[CLIPID]?collectionId=[COLLECTIONID]
Handle Links in Your App#
StorytellerSDK provides two framework-independent methods:
let url = URL(string: "https://example.shar.estori.es/open/story-id/page-id")!
let isStorytellerLink = Storyteller.shared.isStorytellerDeepLink(url: url)
This method takes in a URL and returns true if the URL is Storyteller deep link.
let url = URL(string: "https://example.shar.estori.es/open/story-id/page-id")!
try await Storyteller.shared.openDeepLink(url: url)
This method opens the Story/Clip that was specified in the URL.
Examples#
Choose the URL-receiving route for your app. UIKit receives Universal Links and custom schemes through app- or scene-delegate methods, depending on the lifecycle your app uses. SwiftUI receives both link types through .onOpenURL.
Add handling deep link to your AppDelegate or UISceneDelegate#
Use UIApplicationDelegate when your app owns lifecycle handling there:
import UIKit
import StorytellerSDK
final class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL else {
return false
}
return openStorytellerURL(url)
}
func application(
_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
) -> Bool {
openStorytellerURL(url)
}
private func openStorytellerURL(_ url: URL) -> Bool {
guard Storyteller.shared.isStorytellerDeepLink(url: url) else {
return false
}
Task { @MainActor in
do {
try await Storyteller.shared.openDeepLink(url: url)
} catch {
print("Unable to open Storyteller link: \(error.localizedDescription)")
}
}
return true
}
}
If your app uses scenes, add cold-start handling to your existing scene(_:willConnectTo:options:) implementation and keep the continuation methods for links received while the scene is already connected:
import UIKit
import StorytellerSDK
final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
if let url = connectionOptions.userActivities.lazy
.filter({ $0.activityType == NSUserActivityTypeBrowsingWeb })
.compactMap(\.webpageURL)
.first(where: { Storyteller.shared.isStorytellerDeepLink(url: $0) }) {
openStorytellerURL(url)
return
}
guard let url = connectionOptions.urlContexts.lazy
.map(\.url)
.first(where: { Storyteller.shared.isStorytellerDeepLink(url: $0) })
else {
return
}
openStorytellerURL(url)
}
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL else {
return
}
openStorytellerURL(url)
}
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
guard let url = URLContexts.lazy
.map(\.url)
.first(where: { Storyteller.shared.isStorytellerDeepLink(url: $0) })
else {
return
}
openStorytellerURL(url)
}
private func openStorytellerURL(_ url: URL) {
guard Storyteller.shared.isStorytellerDeepLink(url: url) else { return }
Task { @MainActor in
do {
try await Storyteller.shared.openDeepLink(url: url)
} catch {
print("Unable to open Storyteller link: \(error.localizedDescription)")
}
}
}
}
For a UIKit app entrypoint using CocoaPods, see the Showcase AppDelegate.
Apply .onOpenURL to a stable root view. SwiftUI sends both Universal Links and custom URL schemes to this modifier.
import StorytellerSDK
import SwiftUI
@available(iOS 14.0, *)
struct StorytellerAppRootView: View {
var body: some View {
Text("App content")
.onOpenURL { url in
openStorytellerURL(url)
}
}
private func openStorytellerURL(_ url: URL) {
guard Storyteller.shared.isStorytellerDeepLink(url: url) else { return }
Task { @MainActor in
do {
try await Storyteller.shared.openDeepLink(url: url)
} catch {
print("Unable to open Storyteller link: \(error.localizedDescription)")
}
}
}
}
The Showcase app demonstrates forwarding the received URL from .onOpenURL into a shared AppDelegate handler.
After completing the shared setup and one framework route, test both an HTTPS Universal Link and your tenant's custom URL scheme.
Handling URL Scheme Links from Push Notifications#
When using push notifications to deep link into Storyteller content, you must use URL scheme links (not Universal Links) in your notification payload. Here's how to implement this:
Push Notification Payload#
Include a custom URL scheme link in your push notification payload:
{
"aps": {
"alert": {
"title": "Check out this story!",
"body": "Tap to view the latest content"
}
},
"deeplink_url": "[tenant_name]stories://open/STORY_ID/PAGE_ID"
}
Handling the Deep Link#
The notification should open the custom-scheme URL through the same framework route configured in Handle Links in Your App:
- UIKit routes the URL to
application(_:open:options:). - UIKit apps using scenes route it to
scene(_:openURLContexts:)instead. - SwiftUI routes the URL to
.onOpenURL.
There is no second Storyteller integration path for push notifications. Extract the URL from the notification payload, ask the system to open it, and let your existing URL handler validate and open the Storyteller content.
Extracting Deep Links from Push Notifications#
In your UNUserNotificationCenterDelegate:
@MainActor
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async {
let userInfo = response.notification.request.content.userInfo
guard
let deepLink = userInfo["deeplink_url"] as? String,
let url = URL(string: deepLink)
else {
return
}
// This will trigger onOpenURL in SwiftUI or application(_:open:options:) in UIKit
await UIApplication.shared.open(url)
}
Deep Link Handling Details#
The Storyteller.shared.openDeepLink function intelligently parses the provided URL (which can be either an HTTPS link via Associated Domains or a custom scheme link) to determine the type of content to open.
Story Category#
- Identifies links containing
/open/category/or/go/category/in the path. - Extracts the category identifier following
/category/. - Calls the internal equivalent of
Storyteller.shared.openCategorywith the extracted category ID. - Example HTTPS:
https://[tenantname].shar.estori.es/go/category/123456 - Example Custom Scheme:
[tenantname]stories://open/category/123456
Clip Collection#
- Identifies links containing
/open/clip,/go/clip,/open/clips, or/go/clipsin the path. - Requires a
collectionIdquery parameter. - Optionally accepts a
categoryIdquery parameter to specify an initial category. - Optionally accepts a
clipIdpath segment to attempt opening a specific clip within the collection. - Calls the internal equivalent of
Storyteller.shared.openCollectionusing the extracted information. - Example HTTPS:
https://[tenantname].shar.estori.es/open/clip/CLIP_UUID?collectionId=COLLECTION_ID&categoryId=CATEGORY_ID - Example Custom Scheme:
[tenantname]stories://open/clip/CLIP_UUID?collectionId=COLLECTION_ID&categoryId=CATEGORY_ID
Story / Page#
- Identifies links matching patterns like
/story/STORY_IDor/page/PAGE_ID(for HTTPS) oropen/STORY_ID/PAGE_ID(for custom scheme). - Extracts the
storyIdand/orpageIdfrom the path segments. - Calls the internal equivalent of
Storyteller.shared.openStory(id:)orStoryteller.shared.openPage(id:). - Example HTTPS (Story):
https://[tenantname].shar.estori.es/story/STORY_UUID - Example HTTPS (Page):
https://[tenantname].shar.estori.es/page/PAGE_UUID - Example Custom Scheme:
[tenantname]stories://open/STORY_UUID/PAGE_UUID
Sheet#
- Identifies links containing
/open/sheet/or/go/sheet/in the path. - Extracts the
sheetIdfrom the path segment following/sheet/. - Calls the internal equivalent of
Storyteller.shared.openSheet(id:). - Example HTTPS:
https://[tenantname].ope.nstori.es/open/sheet/SHEET_ID - Example Custom Scheme:
[tenantname]stories://open/sheet/SHEET_ID
Manual Deep Link Handling#
While Storyteller.shared.openDeepLink provides convenience, you might require more control over your app's state or navigation when a deep link is handled. In such cases, it's recommended to parse the URL yourself (after checking it with Storyteller.shared.isStorytellerDeepLink) and then use the specific Storyteller methods like openStory(id:), openPage(id:), openCollection(configuration:), openCategory(category:), or openSheet(id:) to present the content. This approach allows for custom transitions, loading states, or error handling specific to your application flow. Refer to the Open Player documentation for details on these methods.
API Reference#
isStorytellerDeepLink#
let url = URL(string: "exampletenantstories://open/story-id/page-id")!
let isStorytellerLink = Storyteller.shared.isStorytellerDeepLink(url: url)
Checks if the given url is Storyteller deep link.
openDeepLink#
let url = URL(string: "exampletenantstories://open/story-id/page-id")!
try await Storyteller.shared.openDeepLink(url: url)
This call makes Storyteller open the provided deep link (showing the requested Page / Story / Clip).
Parameters:
url- deep link url.
Throws if there is an issue with opening the Deeplink (e.g. the requested content is not available).