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.
-
Create a new space
Log in to Storyblok, select + Add Space → New Space, and follow the steps.
-
Create an Xcode project
Create a new project in Xcode following the official 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
Section titled “Add dependencies”In Xcode, select File → Add Package Dependencies… and enter the following package URL into the search field.
https://github.com/storyblok/storyblok-swift.gitSelect Add Package, then add the StoryblokClient and RichTextView libraries to the Example target.
Create a block type
Section titled “Create a block type”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:
import StoryblokClient
@BlockLibraryenum 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
unknowncase is optional, it acts as a catch-all: any block type not matched by another case decodes as.unknowninstead of failing.
Fetch a single story
Section titled “Fetch a single story”Replace the following in ExampleApp.swift to fetch the preconfigured home story from the space:
import SwiftUIimport Combineimport StoryblokClientinternal import URLSessionExtension
@mainstruct 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 CombinePublisherthat emits aStoryfirst from cache (if present), then from the network (once connected). - Next, the app subscribes to the publisher with the
onReceivemodifier, updating thestorystate. 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
buildExpressionrequires thatBlockconform toView
Render the block type
Section titled “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.
import SwiftUIimport 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
Blockis itself aView, the nested blocks insidepageandgridare rendered by placing them directly in the view hierarchy. - The
unknowncase 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
Section titled “Related resources”Was this page helpful?
This site uses reCAPTCHA and Google's Privacy Policy (opens in a new window).Terms of Service (opens in a new window) apply.
Get in touch with the Storyblok community