Skip to content

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

Set up the project and create a Storyblok space.

  1. Create a new space

    Log in to Storyblok, select + Add SpaceNew 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 guide. Select the iOSApp template to get a minimal SwiftUI setup and name the project Example.

In Xcode, select FileAdd Package Dependencies… and enter the following package URL into the search field.

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

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

Apply the @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
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
}

The @BlockLibrary macro synthesizes the Decodable conformance so the cases can be deserialized from the Content Delivery API:

  • 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.

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

Example/ExampleApp.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 }
}
}
}

With a StoryblokClient initialized with the Block type:

  • The story() function is used to retrieve a Combine Publisher that emits a Story first from cache (if present), then from the network (once connected).
  • Next, the app subscribes to the publisher with the onReceive modifier, updating the story 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

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
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.

Was this page helpful?

What went wrong?

This site uses reCAPTCHA and Google's Privacy Policy (opens in a new window).Terms of Service (opens in a new window) apply.