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

# Integrate Apple with Storyblok

Use Storyblok to manage the content of your Apple project.

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.

```plaintext
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 Apple](/docs/guides/apple/visual-preview)
