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.

MeetingNavigation.tutorial

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.

  1. Create a new file, Meeting.swift.

    Meeting.swift
    import SwiftUI
    MeetingNavigation-01-code-0001.swift
  2. Create a new View conformance for displaying the details of the meeting. We will need to hold onto both a Meeting and SyncUp so that we can display attendees and transcript.

    Meeting.swift
    import SwiftUI
    
    struct MeetingView: View {
      let meeting: Meeting
      let syncUp: SyncUp
    }
    MeetingNavigation-01-code-0002.swift
  3. Implement the body of the view as a simple form that displays the attendees and transcript.

    Meeting.swift
    import 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)
    }
    MeetingNavigation-01-code-0003.swift

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.

  1. Go to AppFeature.swift where the App reducer is defined. Currently it defines a Path 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
    import ComposableArchitecture
    import SwiftUI
    
    @Reducer
    struct App {
      @Reducer
      enum Path {
        case detail(SyncUpDetail)
      }
      // ...
    }
    MeetingNavigation-02-code-0001.swift
  2. 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 a Meeting and a SyncUp.

    AppFeature.swift
    import ComposableArchitecture
    import SwiftUI
    
    @Reducer
    struct App {
      @Reducer
      enum Path {
        case detail(SyncUpDetail)
        case meeting(Meeting, syncUp: SyncUp)
      }
      // ...
    }
    MeetingNavigation-02-code-0002.swift
  3. In the AppView we now need to add the .meeting case to our switch inside the NavigationStack. Here we can bind to the data in the .meeting case and hand it to the MeetingView.

    AppFeature.swift
    import 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()
        }
      )
    }
    MeetingNavigation-02-code-0003.swift
  4. Next, in SyncUpDetail.swift update the SyncUpDetailView to construct a NavigationLink to the meeting view. This is done by using the special SwiftUI/NavigationLink/init(state:label:fileID:filePath:line:column:) initializer that comes with the Composable Architecture.

    SyncUpDetail.swift
    import 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()
          }
        )
      }
    }
    MeetingNavigation-02-code-0004.swift