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.

SyncUpDetail.tutorial

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.

  1. 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
    import ComposableArchitecture
    import SwiftUI
    
    @Reducer
    struct SyncUpDetail {
      @ObservableState
      struct State: Equatable {
      }
    
      enum Action {
      }
    
      var body: some ReducerOf<Self> {
        Reduce { state, action in
          switch action {
          }
        }
      }
    }
    SyncUpDetail-01-code-0001.swift
  2. Add a syncUp field to the State struct since we need that data to populate the UI. There will be more state in this feature later, but we only need a SyncUp 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 the SyncUpsList feature. This will make it possible for this feature to make edits to the state, and for the data to automatically be updated in SyncUpsList.State.

    SyncUpDetail.swift
    import 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 {
          }
        }
      }
    }
    SyncUpDetail-01-code-0002.swift
  3. Add an action for each of the buttons in the UI, including tapping the “Delete” button, and the “Edit” button.

    SyncUpDetail.swift
    import 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 {
          }
        }
      }
    }
    SyncUpDetail-01-code-0003.swift
  4. 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
    import 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
          }
        }
      }
    }
    SyncUpDetail-01-code-0004.swift
  5. Add a new struct, SyncUpDetailView, that will hold the UI for the new detail feature. It will hold onto a Store of the detail feature so that we can read its state and send actions in the view.

    SyncUpDetail.swift
    import ComposableArchitecture
    import SwiftUI
    
    @Reducer
    struct SyncUpDetail {
      // ...
    }
    
    struct SyncUpDetailView: View {
      let store: StoreOf<SyncUpDetail>
    
      var body: some View {
        Form {
    
        }
      }
    }
    SyncUpDetail-01-code-0005.swift
  6. 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
    import 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)
          }
        }
      }
    }
    SyncUpDetail-01-code-0006.swift
  7. At the bottom of the file add a preview to see that the view does look as we expect.

    SyncUpDetail.swift
    import 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()
          }
        )
      }
    }
    SyncUpDetail-01-code-0007.swift