Navigating to a meeting
Now that we have a navigation stack in place we can start navigating to other features in the application. The simplest is the meeting screen, which allows you to see the details of a previous recorded meeting.
The meeting screen
The meeting screen is a static view that simply displays information. It has no logic or behavior of its own. And for that reason it’s not necessary to create a whole new Composable Architecture feature for this screen. We can use a vanilla SwiftUI view, and then integrate it into the rest of the application.

Create a new file, Meeting.swift.
Meeting.swift
MeetingNavigation-01-code-0001.swiftimport SwiftUI
Create a new
View
conformance for displaying the details of the meeting. We will need to hold onto both aMeeting
andSyncUp
so that we can display attendees and transcript.Meeting.swift
MeetingNavigation-01-code-0002.swiftimport SwiftUI struct MeetingView: View { let meeting: Meeting let syncUp: SyncUp }
Implement the body of the view as a simple form that displays the attendees and transcript.
Meeting.swift
MeetingNavigation-01-code-0003.swiftimport SwiftUI struct MeetingView: View { let meeting: Meeting let syncUp: SyncUp var body: some View { Form { Section { ForEach(syncUp.attendees) { attendee in Text(attendee.name) } } header: { Text("Attendees") } Section { Text(meeting.transcript) } header: { Text("Transcript") } } .navigationTitle(Text(meeting.date, style: .date)) } } #Preview { MeetingView(meeting: SyncUp.mock.meetings[0], syncUp: .mock) }
Navigate to a meeting
With the meeting view in place we can now integrate it
That’s all it takes. We can now navigate to any meeting that has been previously recorded. However, we haven’t yet implemented the record meeting feature. That is by far the most complex feature of the app, and is covered in The RecordMeeting feature, but before getting to that let’s see what it takes to test features that are integrated together in a navigation stack.
Go to AppFeature.swift where the
App
reducer is defined. Currently it defines aPath
reducer that has a case for each feature that can be navigated to in the stack. Currently that’s only the detail feature.AppFeature.swift
MeetingNavigation-02-code-0001.swiftimport ComposableArchitecture import SwiftUI @Reducer struct App { @Reducer enum Path { case detail(SyncUpDetail) } // ... }
Add a case for the meeting view that we want to be able to navigate to. Instead of holding onto the state of some Composable Architecture feature, it can just hold onto the state the
MeetingView
needs to do its job, which is aMeeting
and aSyncUp
.AppFeature.swift
MeetingNavigation-02-code-0002.swiftimport ComposableArchitecture import SwiftUI @Reducer struct App { @Reducer enum Path { case detail(SyncUpDetail) case meeting(Meeting, syncUp: SyncUp) } // ... }
In the
AppView
we now need to add the.meeting
case to ourswitch
inside theNavigationStack
. Here we can bind to the data in the.meeting
case and hand it to theMeetingView
.AppFeature.swift
MeetingNavigation-02-code-0003.swiftimport ComposableArchitecture import SwiftUI @Reducer struct App { // ... } struct AppView: View { @Bindable var store: StoreOf<App> var body: some View { NavigationStack( path: $store.scope(state: \.path, action: \.path) ) { SyncUpsListView( store: store.scope(state: \.syncUpsList, action: \.syncUpsList) ) } destination: { store in switch store.case { case let .detail(detailStore): SyncUpDetailView(store: detailStore) case let .meeting(meeting, syncUp: syncUp): MeetingView(meeting: meeting, syncUp: syncUp) } } } } #Preview { AppView( store: Store( initialState: App.State( syncUpsList: SyncUpsList.State() ) ) { App() } ) }
Next, in SyncUpDetail.swift update the
SyncUpDetailView
to construct aNavigationLink
to the meeting view. This is done by using the specialSwiftUI/NavigationLink/init(state:label:fileID:filePath:line:column:)
initializer that comes with the Composable Architecture.SyncUpDetail.swift
MeetingNavigation-02-code-0004.swiftimport ComposableArchitecture import SwiftUI @Reducer struct SyncUpDetail { // ... } struct SyncUpDetailView: View { @Bindable var store: StoreOf<SyncUpDetail> var body: some View { Form { Section { NavigationLink { } label: { Label("Start Meeting", systemImage: "timer") .font(.headline) .foregroundColor(.accentColor) } HStack { Label("Length", systemImage: "clock") Spacer() Text(store.syncUp.duration.formatted(.units())) } HStack { Label("Theme", systemImage: "paintpalette") Spacer() Text(store.syncUp.theme.name) .padding(4) .foregroundColor(store.syncUp.theme.accentColor) .background(store.syncUp.theme.mainColor) .cornerRadius(4) } } header: { Text("Sync-up Info") } if !store.syncUp.meetings.isEmpty { Section { ForEach(store.syncUp.meetings) { meeting in NavigationLink( state: AppFeature.Path.State.meeting(meeting, syncUp: store.syncUp) ) { HStack { Image(systemName: "calendar") Text(meeting.date, style: .date) Text(meeting.date, style: .time) } } } } header: { Text("Past meetings") } } Section { ForEach(store.syncUp.attendees) { attendee in Label(attendee.name, systemImage: "person") } } header: { Text("Attendees") } Section { Button("Delete") { store.send(.deleteButtonTapped) } .foregroundColor(.red) .frame(maxWidth: .infinity) } } .navigationTitle(Text(store.syncUp.title)) .toolbar { Button("Edit") { store.send(.editButtonTapped) } } .alert($store.scope(state: \.destination?.alert, action: \.destination.alert)) .sheet( item: $store.scope(state: \.destination?.edit, action: \.destination.edit) ) { editSyncUpStore in NavigationStack { SyncUpFormView(store: editSyncUpStore) .navigationTitle(store.syncUp.title) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { store.send(.cancelEditButtonTapped) } } ToolbarItem(placement: .confirmationAction) { Button("Done") { store.send(.doneEditingButtonTapped) } } } } } } } #Preview { NavigationStack { SyncUpDetailView( store: Store( initialState: SyncUpDetail.State( syncUp: Shared(value: .mock) ) ) { SyncUpDetail() } ) } }