# iOS Quickstart Guide URL: /Quickstart/ ## Task You are helping an engineer get the first Storyteller content on screen in a native iOS app. Keep initialization, content, and layout failures distinct. Use the complete UIKit or SwiftUI journey and point advanced integrations to the detailed component docs and Showcase app. ## Metadata - Slug: `quickstart` - Source: `public-docs/Quickstart.md` - Audience: First-time iOS SDK integrators - Platforms: `iOS` - Related: `StorytellerListViews.md`, `Users.md`, `PrivacyAndTracking.md`, `Themes.md` ## Overview - The current SDK supports an iOS 13.0+ deployment target and is tested with Xcode 26.2. Earlier Xcode versions may also work, but they are not part of our tested configuration. The private Showcase apps target iOS 16 and have separate build settings; see `https://docs.getstoryteller.com/ios/#sdk-and-showcase-requirements`. - Integration also requires a tenant API key, a category identifier from the same tenant/environment, a stable non-PII user ID, and at least one currently published Story in that category. - Linked Showcase implementations require separate access to the private customer repository through an authorised GitHub account. - Swift Package Manager is the recommended installation path. CocoaPods and manual XCFramework instructions remain available. - Both UIKit and SwiftUI examples await `Storyteller.shared.initialize(...)` before configuring or loading content. - The UIKit example gives `StorytellerStoriesRowView` explicit Auto Layout constraints and a height. - The SwiftUI example creates `StorytellerStoriesListModel` only after initialization and gives `StorytellerStoriesRow` an explicit height. - Both examples map callbacks into visible initializing, loading, empty, failure, and success states. - The Quickstart stops after first success and links to the existing richer Showcase implementations rather than introducing another sample app. ## When To Use - You are integrating Storyteller into a native iOS app for the first time. - An existing integration displays a blank list and you need to isolate initialization, content, or layout. - You need complete, compile-checked UIKit or SwiftUI code before moving to advanced list configuration. ## Prerequisites - Obtain `` and `` from your Storyteller contact or existing tenant content setup. - Confirm that the API key and category belong to the same tenant and environment. - Publish at least one Story in the category; draft, scheduled, expired, or otherwise unavailable content does not appear. - Replace `` with a stable, non-personally-identifiable user identifier. - If you plan to use the linked implementation examples, confirm your authorised GitHub account can access the private iOS Showcase source; see `https://docs.getstoryteller.com/ios/#showcase-source-access`. ## Integration Steps 1. Install `StorytellerSDK`; prefer Swift Package Manager. 2. Replace ``, ``, and ``. 3. Await `Storyteller.shared.initialize(...)` and handle any thrown error. 4. Configure a Stories list with `StorytellerStoriesListConfiguration(categories:)`. 5. Give the row a non-zero layout. 6. Load the row and observe `onDataLoadComplete` or `StorytellerListAction.onDataLoadComplete`. 7. Treat `dataCount > 0` as first-content success, zero items as an empty/content-configuration result, and a returned error as a load failure. ## API Cheat Sheet - `Storyteller.shared.initialize(apiKey:userInput:eventTrackingOptions:)` - Main-actor, `async throws` initialization boundary. - `isInitialized` becomes `true` only after success and resets to `false` when initialization starts again. - Stable public `StorytellerError` cases are `networkError(Error)`, `contentNotFound(String)`, and `wrongInputData`; retain a general `catch` path for underlying transport or decoding errors. - `StorytellerUserInput(externalId:)` - Identifies the user with a stable, non-personally-identifiable external ID. - `StorytellerStoriesListConfiguration(categories:)` - Configures a Stories row or grid for category identifiers in the initialized tenant. - `StorytellerStoriesRowView` - UIKit surface. Assign a delegate, configure it, give it a non-zero layout, then call `reloadData()`. - `StorytellerStoriesListModel` and `StorytellerStoriesRow` - SwiftUI model and surface. Inserting the configured model-backed row triggers its initial load. - `onDataLoadComplete(success:error:dataCount:)` - Distinguishes a failed request, an empty successful response, and loaded content. ## Installation Notes - Swift Package Manager URL: `https://github.com/getstoryteller/storyteller-sdk-swift-package`. - Swift Package Manager resolves `StorytellerLottie` 4.6.0 transitively, so it does not need to be added separately. - CocoaPods requires the Storyteller SDK spec source, StorytellerLottie spec source, and CocoaPods CDN; run `pod install`, then open the generated `.xcworkspace`. StorytellerLottie resolves transitively. - Manual installation requires both `StorytellerSDK.xcframework` and `StorytellerLottie.xcframework`, both set to **Embed & Sign**. - Upstream Airbnb Lottie can coexist with the namespaced `StorytellerLottie` dependency, but it does not replace it. ## Initialization-Order Contract - The documented path always awaits initialization before requesting content. - The SDK currently defers an early list reload until initialization succeeds when the list identifier remains unchanged. - A failed initialization attempt leaves the reload pending; a later successful attempt allows it to continue when the identifier still matches. Treat this behavior as a safeguard, not the normal integration sequence. ## Diagnostics - Initialization message: verify API key, tenant/environment, connectivity, and the underlying error. - Load error: initialization succeeded; inspect the content request error and Xcode logs. - Successful zero count: verify category ID, published/scheduled/expired state, and targeting for the current user. - Positive count but no visible tiles: verify UIKit constraints or SwiftUI frame height. - Indefinite loading: verify initialization sequencing, callback lifetime, connectivity, and logs. ## Cross-References - List configuration: `StorytellerListViews.md` - User identity and personalization: `Users.md` - Production tracking choices: `PrivacyAndTracking.md` - Styling: `Themes.md` - SwiftUI list model: https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/main/ShowcaseApp/Views/Home/Components/StorytellerItemViewModel.swift#L9 - SwiftUI action handling: https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/main/ShowcaseApp/Views/Home/Components/StorytellerItemView.swift#L10 - UIKit list example: https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/cocoapods/StorytellerSampleApp/Components/MultipleLists/MultipleListsViewController.swift#L11 ## Canonical Reference # iOS Quickstart Guide This guide takes a new native iOS integration from an empty screen to a visible row of published Storyteller Stories. Choose either the complete UIKit or SwiftUI path below, then use the Showcase app and component guides for more advanced integrations. For Apple TV, use the dedicated [tvOS Guide](tvOS.md). ## Before You Start You need: - An iOS deployment target of 13.0 or later. We test the current SDK line with Xcode 26.2; earlier Xcode versions may also work, but they are not part of our tested configuration. The example apps have separate [Showcase build requirements](index.md#sdk-and-showcase-requirements). - A Storyteller API key for the tenant and environment you are integrating. Ask your Storyteller contact if you do not have one. - A Story category identifier from the same tenant and environment. Ask your Storyteller contact which category should power your first row if this is not already part of your content setup. - At least one Story assigned to that category and published in the Storyteller CMS. Draft, scheduled, expired, or otherwise unavailable content will not appear. - A stable, non-personally-identifiable ID for the current user. The examples use ``; see [Working with Users](Users.md) before choosing a production identifier. - If you plan to use the linked implementation examples, an authorised GitHub account with [access to the private iOS Showcase source](index.md#showcase-source-access). Keep the API key, category identifier, and published content in the same tenant and environment. A valid key cannot load a category from another tenant. ## Install the SDK ### Swift Package Manager (Recommended) 1. In Xcode, open **File > Add Packages…**. 1. Enter `https://github.com/getstoryteller/storyteller-sdk-swift-package`. 1. Add the `StorytellerSDK` product to your app target. The package resolves `StorytellerLottie` 4.6.0 transitively, so you do not need to add `StorytellerLottie` separately. If your app also uses upstream Airbnb Lottie, keep it as its own dependency; its `Lottie` module can coexist with Storyteller's namespaced `StorytellerLottie` module. ### CocoaPods Add the Storyteller sources and SDK to your `Podfile`: ```ruby source 'https://github.com/getstoryteller/storyteller-sdk-ios-podspec.git' source 'https://github.com/getstoryteller/storyteller-lottie-ios-podspec.git' source 'https://cdn.cocoapods.org/' use_frameworks! target 'MyAwesomeApp' do pod 'StorytellerSDK' end ``` Both Storyteller source lines are required: the first contains the SDK podspec and the second contains its `StorytellerLottie` dependency. The CocoaPods CDN supplies public dependencies. Then run `pod install` and open the generated `.xcworkspace`, not the `.xcodeproj`. CocoaPods installs `StorytellerLottie` transitively, so you do not need to add it separately. If your app also uses upstream Airbnb Lottie, keep it as its own dependency; it can coexist with `StorytellerLottie`. ### XCFrameworks 1. Download and unzip [`StorytellerSDK` 11.6.0](https://storyteller.azureedge.net/sdk-ios/xcframeworks/11.6.0/StorytellerSDK.zip). 1. Download and unzip [`StorytellerLottie` 4.6.0](https://storyteller.azureedge.net/sdk-ios/storyteller-lottie/4.6.0/StorytellerLottie.zip). 1. Add `StorytellerSDK.xcframework` and `StorytellerLottie.xcframework` to the same app target. `StorytellerLottie` is required by the SDK. If your app also uses upstream Airbnb Lottie, include it separately; it can coexist with `StorytellerLottie`, but it does not replace it. 1. In the app target's **General > Frameworks, Libraries, and Embedded Content** section, select **Embed & Sign** for both XCFrameworks. ## Add Your First Stories Row Replace these placeholders before running either example: - ``: the API key supplied for your Storyteller tenant and environment. - ``: a stable, non-personally-identifiable identifier for the current user. - ``: a category that contains at least one currently published Story. Both examples deliberately finish `Storyteller.shared.initialize(...)` before configuring and loading the row. They also expose initialization, loading, empty, failure, and success states so a blank screen is not the only diagnostic signal. ### UIKit Use this view controller directly or copy the same initialization, constraints, and delegate handling into your own screen. ```swift import StorytellerSDK import UIKit // MARK: - UIKitQuickstartViewController final class UIKitQuickstartViewController: UIViewController { // MARK: Internal override func viewDidLoad() { super.viewDidLoad() configureLayout() Task { [weak self] in await self?.loadStories() } } // MARK: Private private let statusLabel = UILabel() private let storiesRow = StorytellerStoriesRowView() private func configureLayout() { view.backgroundColor = .systemBackground statusLabel.numberOfLines = 0 statusLabel.text = "Initializing Storyteller…" statusLabel.translatesAutoresizingMaskIntoConstraints = false storiesRow.translatesAutoresizingMaskIntoConstraints = false view.addSubview(statusLabel) view.addSubview(storiesRow) NSLayoutConstraint.activate([ statusLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 24), statusLabel.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor), statusLabel.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor), storiesRow.topAnchor.constraint(equalTo: statusLabel.bottomAnchor, constant: 16), storiesRow.leadingAnchor.constraint(equalTo: view.leadingAnchor), storiesRow.trailingAnchor.constraint(equalTo: view.trailingAnchor), storiesRow.heightAnchor.constraint(equalToConstant: 180), storiesRow.bottomAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.bottomAnchor), ]) } private func loadStories() async { do { try await Storyteller.shared.initialize( apiKey: "", userInput: StorytellerUserInput(externalId: "") ) storiesRow.delegate = self storiesRow.configure( with: StorytellerStoriesListConfiguration( categories: [""] ) ) storiesRow.reloadData() } catch { statusLabel.isHidden = false statusLabel.text = "Storyteller could not initialize: \(error.localizedDescription)" } } } // MARK: StorytellerListViewDelegate extension UIKitQuickstartViewController: StorytellerListViewDelegate { nonisolated func onDataLoadStarted() { Task { @MainActor [weak self] in self?.statusLabel.isHidden = false self?.statusLabel.text = "Loading Stories…" } } nonisolated func onDataLoadComplete(success: Bool, error: Error?, dataCount: Int) { let statusText: String? if let error { statusText = "Stories could not load: \(error.localizedDescription)" } else if !success { statusText = "Stories could not load. Check the Xcode console for details." } else if dataCount == 0 { statusText = "No published Stories were found for this category." } else { statusText = nil } Task { @MainActor [weak self] in self?.statusLabel.isHidden = statusText == nil self?.statusLabel.text = statusText } } } ``` The explicit leading, trailing, top, and height constraints are required. Adding a `StorytellerStoriesRowView` to a hierarchy without giving it a non-zero layout can load data successfully while displaying nothing. ### SwiftUI Place `SwiftUIQuickstartView` anywhere in your SwiftUI hierarchy. The model is created only after initialization succeeds; inserting the row then triggers its initial load. ```swift import StorytellerSDK import SwiftUI struct SwiftUIQuickstartView: View { // MARK: Internal var body: some View { VStack(alignment: .leading, spacing: 16) { statusView if let storiesModel { StorytellerStoriesRow(model: storiesModel) { action in Task { @MainActor in handle(action) } } .frame(height: 180) } } .padding(.vertical, 24) .onAppear { Task { @MainActor in await loadStories() } } } // MARK: Private private enum LoadState { case initializing case loading case empty case ready case failed(String) } @State private var hasStarted = false @State private var loadState = LoadState.initializing @State private var storiesModel: StorytellerStoriesListModel? @ViewBuilder private var statusView: some View { switch loadState { case .initializing: Text("Initializing Storyteller…") case .loading: Text("Loading Stories…") case .empty: Text("No published Stories were found for this category.") case .ready: EmptyView() case .failed(let message): Text(message) } } @MainActor private func loadStories() async { guard !hasStarted else { return } hasStarted = true do { try await Storyteller.shared.initialize( apiKey: "", userInput: StorytellerUserInput(externalId: "") ) loadState = .loading storiesModel = StorytellerStoriesListModel( configuration: StorytellerStoriesListConfiguration( categories: [""] ) ) } catch { loadState = .failed( "Storyteller could not initialize: \(error.localizedDescription)" ) } } @MainActor private func handle(_ action: StorytellerListAction) { switch action { case .onDataLoadStarted: loadState = .loading case let .onDataLoadComplete(success, error, dataCount): if let error { loadState = .failed( "Stories could not load: \(error.localizedDescription)" ) } else if !success { loadState = .failed( "Stories could not load. Check the Xcode console for details." ) } else if dataCount == 0 { loadState = .empty } else { loadState = .ready } default: break } } } ``` Keep the explicit `.frame(height: 180)` or give the row an equivalent non-zero height within your own layout. ## Initialization Order `Storyteller.shared.initialize(...)` is `async throws`. Its `isInitialized` value becomes `true` only after initialization succeeds and is reset to `false` whenever initialization starts again. Across Storyteller's public methods, the stable `StorytellerError` cases are `networkError(Error)`, `contentNotFound(String)`, and `wrongInputData`. Initialization may also surface an underlying transport or response-decoding error, so keep a general `catch` path. {: #sdk-initialization } The SDK currently defers a list reload requested before initialization. If an initialization attempt fails, the reload remains pending and can resume after a later successful attempt, provided the list identifier has not changed. This is a safeguard, not the recommended integration sequence. Await initialization explicitly as shown above. ## Confirm Success A successful load calls `onDataLoadComplete` with `success == true`, no error, and `dataCount > 0`. The status text disappears and a horizontal row of Story tiles appears. Tapping a tile opens the Story Player unless your theme disables Player opening. The tile artwork and shape depend on your tenant content and theme, but the populated row will look similar to the Story tiles in this visual: ## Diagnose a Blank or Failed Result | Observable result | What it establishes | What to check | | --- | --- | --- | | `Storyteller could not initialize` | The content request did not start. | Confirm the API key, tenant/environment, connectivity, and the underlying error in Xcode. | | `Stories could not load` | Initialization succeeded, but the content request failed. | Inspect the returned error and Xcode logs; verify network access and tenant configuration. | | `No published Stories were found` | The request succeeded with `dataCount == 0`. | Confirm ``, category assignment, publication status, schedule/expiry, and any targeting rules for ``. | | `success == true` and `dataCount > 0`, but no tiles are visible | Content loaded and the problem is presentation. | Confirm that the UIKit row has non-zero constraints or that the SwiftUI row has a non-zero frame. | | The screen remains in a loading state | A request has not completed. | Confirm initialization is awaited, keep the delegate/action callback alive, inspect Xcode logs, and check connectivity. | ## Go Further The Quickstart intentionally stops after the first reliable row. Continue with: - [Troubleshooting Storyteller on iOS](Troubleshooting.md) when you need to distinguish app integration, content, tenant, provider, and SDK causes from an observable symptom. - [Storyteller List Views](StorytellerListViews.md) for grids, Clips rows, display limits, themes, and analytics context. - [Working with Users](Users.md) for external IDs, personalization, locale, and followed categories. - [Privacy and Tracking](PrivacyAndTracking.md) before selecting production tracking options. - [Custom Themes](Themes.md) for tile and Player styling. - The Showcase app's [SwiftUI list model](https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/main/ShowcaseApp/Views/Home/Components/StorytellerItemViewModel.swift#L9), [SwiftUI action handling](https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/main/ShowcaseApp/Views/Home/Components/StorytellerItemView.swift#L10), and [UIKit list example](https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.0/cocoapods/StorytellerSampleApp/Components/MultipleLists/MultipleListsViewController.swift#L11) for richer, production-shaped examples.