Sync-up detail
The “sync-up detail” screen shows all the information of a sync-up, and has many actions that can take place in the screen. The user can edit the sync-up, or start a new meeting in the sync up, or drill-down to a past meeting, or even delete the meeting.
Create the sync-up detail
Let’s get the basics of a sync-up detail screen into place. It won’t have much functionality from the beginning, but we will slowly layer on the feature’s full logic and behavior.

Next we add a case to the Action
enum for each action the user can perform in the view. Currently there are 2 buttons, and so we will have an action for each.
Create a new file called SyncUpDetail.swift that will hold the reducer and view for the feature. Paste in the basic scaffolding of a new
SyncUpDetail
reducer.SyncUpDetail.swift
SyncUpDetail-01-code-0001.swiftimport ComposableArchitecture import SwiftUI @Reducer struct SyncUpDetail { @ObservableState struct State: Equatable { } enum Action { } var body: some ReducerOf<Self> { Reduce { state, action in switch action { } } } }
Add a
syncUp
field to theState
struct since we need that data to populate the UI. There will be more state in this feature later, but we only need aSyncUp
for now.Further, this field will be annotated with the
@Shared
property wrapper in order to indicate that state is shared with another feature, in particular theSyncUpsList
feature. This will make it possible for this feature to make edits to the state, and for the data to automatically be updated inSyncUpsList.State
.SyncUpDetail.swift
SyncUpDetail-01-code-0002.swiftimport ComposableArchitecture import SwiftUI @Reducer struct SyncUpDetail { @ObservableState struct State: Equatable { @Shared var syncUp: SyncUp } enum Action { } var body: some ReducerOf<Self> { Reduce { state, action in switch action { } } } }
Add an action for each of the buttons in the UI, including tapping the “Delete” button, and the “Edit” button.
SyncUpDetail.swift
SyncUpDetail-01-code-0003.swiftimport ComposableArchitecture import SwiftUI @Reducer struct SyncUpDetail { @ObservableState struct State: Equatable { @Shared var syncUp: SyncUp } enum Action { case deleteButtonTapped case editButtonTapped } var body: some ReducerOf<Self> { Reduce { state, action in switch action { } } } }
Add each of the new action cases to the reducer. Currently we are not capable of implementing any of the logic for these actions. We will need to do more work later.
SyncUpDetail.swift
SyncUpDetail-01-code-0004.swiftimport ComposableArchitecture import SwiftUI @Reducer struct SyncUpDetail { @ObservableState struct State: Equatable { @Shared var syncUp: SyncUp } enum Action { case deleteButtonTapped case editButtonTapped } var body: some ReducerOf<Self> { Reduce { state, action in switch action { case .deleteButtonTapped: return .none case .editButtonTapped: return .none } } } }
Add a new struct,
SyncUpDetailView
, that will hold the UI for the new detail feature. It will hold onto aStore
of the detail feature so that we can read its state and send actions in the view.SyncUpDetail.swift
SyncUpDetail-01-code-0005.swiftimport ComposableArchitecture import SwiftUI @Reducer struct SyncUpDetail { // ... } struct SyncUpDetailView: View { let store: StoreOf<SyncUpDetail> var body: some View { Form { } } }
Paste in the body of the view. There is nothing too special in this view code, and it’s mostly taken verbatim from Apple’s Scrumdinger application. The most important lesson in this code is that we only ever read data from the
store
and send actions to it. We should not be performing any significant logic in the view. That should all go in the reducer.SyncUpDetail.swift
SyncUpDetail-01-code-0006.swiftimport ComposableArchitecture import SwiftUI @Reducer struct SyncUpDetail { // ... } struct SyncUpDetailView: View { let 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 Button { } label: { 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) } } } }
At the bottom of the file add a preview to see that the view does look as we expect.
SyncUpDetail.swift
SyncUpDetail-01-code-0007.swiftimport ComposableArchitecture import SwiftUI @Reducer struct SyncUpDetail { // ... } struct SyncUpDetailView: View { let 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 Button { } label: { 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) } } } } #Preview { NavigationStack { SyncUpDetailView( store: Store( initialState: SyncUpDetail.State( syncUp: Shared(value: .mock) ) ) { SyncUpDetail() } ) } }