---
title: Integrate iOS with Storyblok
description: This guide walks you through integrating Storyblok as a headless CMS, fetching content, and building SwiftUI views to render it in your iOS project.
url: https://www.storyblok.com/docs/guides/apple-platforms
---

# Integrate iOS with Storyblok

Use Storyblok to manage content for your apps on Apple platforms. This guide uses iOS as the example, but the same approach works for iPadOS, macOS, watchOS, tvOS, and visionOS.

This guide has been tested with the following versions:

-   Xcode `26.2`
-   Storyblok Swift `0.3.0`

## Setup

Set up the project and create a Storyblok space.

[No Storyblok account?](https://app.storyblok.com/#/signup)Sign up and create a space for free

1.  **Create a [new space](https://app.storyblok.com/#/me/spaces/new?tab=select-plan)**
    
    Log in to [Storyblok](https://app.storyblok.com/#/me/spaces), select **\+ Add Space** → **New Space**, and follow the steps.
    
2.  **Create an Xcode project**
    
    Create a new project in Xcode following the official [Creating an Xcode project for an app](https://developer.apple.com/documentation/xcode/creating-an-xcode-project-for-an-app) guide. Select the **iOS** → **App** template to get a minimal SwiftUI setup and name the project `Example`.
    

## Add dependencies

In Xcode, select **File** → **Add Package Dependencies…** and enter the following package URL into the search field.

```text
https://github.com/storyblok/storyblok-swift.git
```

Select **Add Package**, then add the `StoryblokClient` and `RichTextView` libraries to the `Example` target.

## Create a block type

Apply the [`@BlockLibrary`](https://storyblok.github.io/storyblok-swift/documentation/storyblokclient/blocklibrary\(\)/) macro to an enum with cases for each of the preconfigured blocks defined in the block library of the space.

Create a new `BlockLibrary.swift` file:

Example/BlockLibrary.swift

```swift
import StoryblokClient

@BlockLibrary
enum Block: Decodable, Hashable {
    case feature(name: String)
    case grid(columns: [Block])
    case page(body: [Block])
    case teaser(headline: String)
    case unknown
}
```

> [!WARNING]
> The case name must match the technical name given to the block in the block library.

The [`@BlockLibrary`](https://storyblok.github.io/storyblok-swift/documentation/storyblokclient/blocklibrary\(\)/) macro synthesizes the `Decodable` conformance so the cases can be deserialized from the [Content Delivery API](https://www.storyblok.com/docs/api/content-delivery/v2):

-   The labeled associated values of a case decode the individual fields of the block.
-   The parameterless `unknown` case is optional, it acts as a catch-all: any block type not matched by another case decodes as `.unknown` instead of failing.

> [!NOTE]
> The first time you build, Xcode may ask you to trust and enable the macro provided by the storyblok-swift package.

## Fetch a single story

Replace the following in `ExampleApp.swift` to fetch the preconfigured home story from the space:

> [!TIP]
> Learn how to get an [access token](https://www.storyblok.com/docs/concepts/access-tokens#content-delivery-api-access-tokens) for your Storyblok project.

Example/ExampleApp.swift

```swift
import SwiftUI
import Combine
import StoryblokClient
internal import URLSessionExtension

@main
struct ExampleApp: App {

    private let client = StoryblokClient(
        library: Block.self,
        accessToken: "YOUR_ACCESS_TOKEN",
        version: .draft,
        region: .eu // Choose the correct region from your Space.
    )

    private let publisher: AnyPublisher<Story<Block>, Never>

    init() {
        publisher = client.story("home")
            .receive(on: DispatchQueue.main)
            .catch { error in Empty() }
            .eraseToAnyPublisher()
    }

    @State private var story: Story<Block>?

    var body: some Scene {
        WindowGroup {
            ContentView()
            ScrollView {
                if let story {
                    story.content
                } else {
                    ProgressView()
                        .progressViewStyle(.circular)
                        .frame(maxWidth: .infinity, minHeight: 200)
                }
            }
            .onReceive(publisher) { value in story = value }
        }
    }
}
```

> [!WARNING]
> Ensure to set the correct [`region`](https://storyblok.github.io/storyblok-swift/documentation/urlsessionextension/api/region) value depending on the server location of your Storyblok space.

With a [`StoryblokClient`](https://storyblok.github.io/storyblok-swift/documentation/storyblokclient/storyblokclient/) initialized with the `Block` type:

-   The [`story()`](https://storyblok.github.io/storyblok-swift/documentation/storyblokclient/storyblokclient/) function is used to retrieve a Combine [`Publisher`](https://developer.apple.com/documentation/combine/publisher) that emits a [`Story`](https://storyblok.github.io/storyblok-swift/documentation/storyblokclient/story/) first from cache (if present), then from the network (once connected).
-   Next, the app subscribes to the publisher with the [`onReceive`](https://developer.apple.com/documentation/swiftui/view/onreceive\(_:perform:\)) modifier, updating the `story` [state](https://developer.apple.com/documentation/swiftui/state). Its content is then placed directly in the view hierarchy.

Building the app now informs us of the last step required to render the content:

> Static method `buildExpression` requires that `Block` conform to `View`

## Render the block type

Create a new `BlockView.swift` file that conforms the `Block` library to SwiftUI’s `View` protocol, providing a view for each of its cases.

Example/BlockView.swift

```swift
import SwiftUI
import StoryblokClient

extension Block: View {
    var body: some View {
        switch self {
        case let .page(body):
            LazyVStack(alignment: .leading, spacing: 0) {
                ForEach(body, id: \.self) { $0 }
            }
        case let .feature(name):
            Text(name)
                .padding()
        case let .teaser(headline):
            Text(headline)
                .font(.title)
                .padding()
        case let .grid(columns):
            HStack(alignment: .top) {
                ForEach(columns, id: \.self) { $0 }
            }
        case .unknown: Text("Unknown block type")
        }
    }
}
```

The `View` conformance drives the dynamic block rendering:

-   Because each `Block` is itself a `View`, the nested blocks inside `page` and `grid` are rendered by placing them directly in the view hierarchy.
-   The `unknown` case renders fallback content for any block type not known to the app.

Run the app and see the complete home story with the teaser, feature, and grid components rendered.

## Related resources

[Storyblok Swift Client SDK Reference](https://storyblok.github.io/storyblok-swift/documentation/storyblokclient/)

[Concept: Blocks](/docs/concepts/blocks)

[Content Delivery API: Retrieve a Single Story](/docs/api/content-delivery/v2/stories/retrieve-a-single-story)

[Apple Developer: SwiftUI Documentation](https://developer.apple.com/documentation/swiftui)

[Receiving and Handling Events with Combine](https://developer.apple.com/documentation/combine/receiving-and-handling-events-with-combine)

## Pagination

-   [Next: Visual Preview in iOS](/docs/guides/apple-platforms/visual-preview)
