Skip to content

Dynamic Navigation in Apple

Use NavigationStack to set up a catch-all navigation strategy in your Apple project and render new stories dynamically.

Create a StoryView.swift file to define a reusable view that can display any story, fetched by either its slug or UUID.

Example/StoryView.swift
import SwiftUI
import Combine
import StoryblokClient
struct StoryView<Content: Decodable & View>: View {
private let publisher: AnyPublisher<Story<Content>, Never>
@State private var story: Story<Content>?
init(
_ publisher: AnyPublisher<Story<Content>, StoryblokClient<Block>.Error>,
story: Story<Content>? = nil
) {
self.publisher = publisher
.receive(on: DispatchQueue.main)
.catch { error in Just(story).compactMap { $0 } }
.eraseToAnyPublisher()
self._story = State(initialValue: story)
}
var body: some View {
ScrollView {
if let story {
story.content
} else {
ProgressView()
.progressViewStyle(.circular)
.frame(maxWidth: .infinity, minHeight: 200)
}
}
.onReceive(publisher) { value in story = value }
.refreshable {
for await value in publisher.values {
story = value
}
}
}
}

StoryView moves the story fetching and pull-to-refresh implementation into a reusable view that can display any story publisher. It can be initialized with a Story instance for immediate display when navigating between screens.

In your ExampleApp.swift file, replace the inline fetching implementation with a NavigationStack rendering a StoryView, with a navigation destination for each way a story can be identified.

Example/ExampleApp.swift
import SwiftUI
import Combine
import StoryblokClient
internal import URLSessionExtension
@main
struct ExampleApp: App {
@State private var path = NavigationPath()
private let client = StoryblokClient(
library: Block.self,
accessToken: "YOUR_ACCESS_TOKEN",
version: {
#if DEBUG
.draft
#else
.published
#endif
}(),
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 {
ScrollView {
if let story {
story.content
} else {
ProgressView()
.progressViewStyle(.circular)
.frame(maxWidth: .infinity, minHeight: 200)
}
}
.onReceive(publisher) { value in story = value }
.refreshable {
for await value in publisher.values {
story = value
}
}
NavigationStack(path: $path) {
StoryView<Block>(client.story("home"))
.navigationDestination(for: String.self) { slug in
StoryView<Block>(client.story(slug))
}
.navigationDestination(for: UUID.self) { uuid in
StoryView<Block>(client.story(uuid))
}
.navigationDestination(for: Story<Block>.self) { story in
StoryView(client.story(story.uuid), story: story)
}
}
}
}
}

The three navigationDestination modifiers together handle every possible story destination:

  • A String value fetches the story by its slug, and a UUID value fetches the story by its UUID.
  • A Story value additionally populates the initial value of StoryView with the story instance so available content appears immediately while the latest version is fetched.

With this approach, the project automatically handles new stories that are added to the space and pushed onto the navigation path.

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.