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 story view
Section titled “Create a story view”Create a StoryView.swift file to define a reusable view that can display any story, fetched by either its slug or UUID.
import SwiftUIimport 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.
Set up the navigation stack
Section titled “Set up the navigation stack”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.
import SwiftUIimport Combineimport StoryblokClientinternal import URLSessionExtension
@mainstruct 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
Stringvalue fetches the story by its slug, and aUUIDvalue fetches the story by its UUID. - A
Storyvalue additionally populates the initial value ofStoryViewwith 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.
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