Editing and deleting a sync-up

Let’s implement two major pieces of functionality in the detail screen: editing the sync-up and deleting the sync-up. This will force us to face a number of common problems in app development all at once, including view reuse, navigation, alerts, and parent-child communication patterns.

EditingAndDeletingSyncUp.tutorial

Editing the sync-up

To edit the sync-up we will reuse the SyncUpFormView that we previously used for adding a sync-up. This will allow us to see how easy it is to reuse features in the Composable Architecture, and it will allow us to explore how to communicate from the child feature back to the parent.

We want to present the SyncUpFormView from the SyncUpDetailView in a sheet, and so we will need to use the presentation tools of the library just as we did in the SyncUpListsView.

The domains of the child and parent features are now fully integrated together. We can now implement the logic that causes the edit sheet to be presented.

That is the basics of presenting the “Edit sync-up” sheet from the sync-up detail view. There will be more to do in this reducer in a moment, but we can leave it here for now and concentrate on the view layer for a moment.

We have now finished integrating the SyncUpForm and SyncUpDetail at the view layer, and so all that is left is to finish integrating the features at the reducer level.

  1. Go to the SyncUpDetail.swift file and update the State of the feature so that it holds on to an optional piece of SyncUpForm.State using the Presents macro. When the state is non-nil the sheet will be presented, and when it is nil it will be dismissed.

    SyncUpDetail.swift
    import ComposableArchitecture
    import SwiftUI
    
    @Reducer
    struct SyncUpDetail {
      @ObservableState
      struct State: Equatable {
        @Presents var editSyncUp: SyncUpForm.State?
        @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
          }
        }
      }
    }
    
    struct SyncUpDetailView: View {
      // ...
    }
    EditingAndDeletingSyncUp-01-code-0001.swift
  2. Add a case to the Action enum to represent the PresentationAction of the SyncUpForm.

    SyncUpDetail.swift
    import ComposableArchitecture
    import SwiftUI
    
    @Reducer
    struct SyncUpDetail {
      @ObservableState
      struct State: Equatable {
        @Presents var editSyncUp: SyncUpForm.State?
        @Shared var syncUp: SyncUp
      }
    
      enum Action {
        case deleteButtonTapped
        case editButtonTapped
        case editSyncUp(PresentationAction<SyncUpForm.Action>)
      }
    
      var body: some ReducerOf<Self> {
        Reduce { state, action in
          switch action {
          case .deleteButtonTapped:
            return .none
    
          case .editButtonTapped:
            return .none
          }
        }
      }
    }
    
    struct SyncUpDetailView: View {
      // ...
    }
    EditingAndDeletingSyncUp-01-code-0002.swift
  3. Integrate the SyncUpForm reducer into the SyncUpDetail reducer by using the ComposableArchitecture/Reducer/ifLet(_:action:destination:fileID:filePath:line:column:)-4ub6q operator.

    SyncUpDetail.swift
    import ComposableArchitecture
    import SwiftUI
    
    @Reducer
    struct SyncUpDetail {
      @ObservableState
      struct State: Equatable {
        @Presents var editSyncUp: SyncUpForm.State?
        @Shared var syncUp: SyncUp
      }
    
      enum Action {
        case deleteButtonTapped
        case editButtonTapped
        case editSyncUp(PresentationAction<SyncUpForm.Action>)
      }
    
      var body: some ReducerOf<Self> {
        Reduce { state, action in
          switch action {
          case .deleteButtonTapped:
            return .none
    
          case .editButtonTapped:
            return .none
    
          case .editSyncUp:
            return .none
          }
        }
        .ifLet(\.$editSyncUp, action: \.editSyncUp) {
          SyncUpForm()
        }
      }
    }
    
    struct SyncUpDetailView: View {
      // ...
    }
    EditingAndDeletingSyncUp-01-code-0003.swift
  4. When the “Edit” button is tapped we can populate the editSyncUp state to represent the sheet should be presented. We can pass along the local syncUp value to the SyncUpForm so that it has the freshest data to edit.

    SyncUpDetail.swift
    import ComposableArchitecture
    import SwiftUI
    
    @Reducer
    struct SyncUpDetail {
      @ObservableState
      struct State: Equatable {
        @Presents var editSyncUp: SyncUpForm.State?
        @Shared var syncUp: SyncUp
      }
    
      enum Action {
        case deleteButtonTapped
        case editButtonTapped
        case editSyncUp(PresentationAction<SyncUpForm.Action>)
      }
    
      var body: some ReducerOf<Self> {
        Reduce { state, action in
          switch action {
          case .deleteButtonTapped:
            return .none
    
          case .editButtonTapped:
            state.editSyncUp = SyncUpForm.State(syncUp: state.syncUp)
            return .none
    
          case .editSyncUp:
            return .none
          }
        }
        .ifLet(\.$editSyncUp, action: \.editSyncUp) {
          SyncUpForm()
        }
      }
    }
    
    struct SyncUpDetailView: View {
      // ...
    }
    EditingAndDeletingSyncUp-01-code-0004.swift
  5. Update the store property in the view to be @Bindable since we will need to derive bindings from the store for driving navigation from state.

    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)
                .foregroundStyle(Color.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)
                .foregroundStyle(store.syncUp.theme.accentColor)
                .background(store.syncUp.theme.mainColor)
                .clipShape(.rect(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", role: .destructive) {
              store.send(.deleteButtonTapped)
            }
            .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()
          }
        )
      }
    }
    EditingAndDeletingSyncUp-01-code-0005.swift
  6. At the very bottom of the view use the sheet(item:) modifier by deriving a binding to the SyncUpForm domain using SwiftUI/Binding/scope(state:action:fileID:filePath:line:column:).

    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)
                .foregroundStyle(Color.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)
                .foregroundStyle(store.syncUp.theme.accentColor)
                .background(store.syncUp.theme.mainColor)
                .clipShape(.rect(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", role: .destructive) {
              store.send(.deleteButtonTapped)
            }
            .frame(maxWidth: .infinity)
          }
        }
        .navigationTitle(Text(store.syncUp.title))
        .toolbar {
          Button("Edit") {
            store.send(.editButtonTapped)
          }
        }
        .sheet(item: $store.scope(state: \.editSyncUp, action: \.editSyncUp)) { editSyncUpStore in
          NavigationStack {
            SyncUpFormView(store: editSyncUpStore)
          }
        }
      }
    }
    
    #Preview {
      NavigationStack {
        SyncUpDetailView(
          store: Store(
            initialState: SyncUpDetail.State(
              syncUp: Shared(value: .mock)
            )
          ) {
            SyncUpDetail()
          }
        )
      }
    }
    EditingAndDeletingSyncUp-01-code-0006.swift
  7. Provide a navigation title and toolbar buttons to the sheet for saving and cancelling the edits made to the sync-up.

    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)
                .foregroundStyle(Color.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)
                .foregroundStyle(store.syncUp.theme.accentColor)
                .background(store.syncUp.theme.mainColor)
                .clipShape(.rect(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", role: .destructive) {
              store.send(.deleteButtonTapped)
            }
            .frame(maxWidth: .infinity)
          }
        }
        .navigationTitle(Text(store.syncUp.title))
        .toolbar {
          Button("Edit") {
            store.send(.editButtonTapped)
          }
        }
        .sheet(item: $store.scope(state: \.editSyncUp, action: \.editSyncUp)) { editSyncUpStore in
          NavigationStack {
            SyncUpFormView(store: editSyncUpStore)
              .navigationTitle(store.syncUp.title)
              .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                  Button("Cancel") {
                  }
                }
                ToolbarItem(placement: .confirmationAction) {
                  Button("Done") {
                  }
                }
              }
          }
        }
      }
    }
    
    #Preview {
      NavigationStack {
        SyncUpDetailView(
          store: Store(
            initialState: SyncUpDetail.State(
              syncUp: Shared(value: .mock)
            )
          ) {
            SyncUpDetail()
          }
        )
      }
    }
    EditingAndDeletingSyncUp-01-code-0007.swift
  8. Send actions to the store when each of the toolbar buttons is tapped.

    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)
                .foregroundStyle(Color.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)
                .foregroundStyle(store.syncUp.theme.accentColor)
                .background(store.syncUp.theme.mainColor)
                .clipShape(.rect(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", role: .destructive) {
              store.send(.deleteButtonTapped)
            }
            .frame(maxWidth: .infinity)
          }
        }
        .navigationTitle(Text(store.syncUp.title))
        .toolbar {
          Button("Edit") {
            store.send(.editButtonTapped)
          }
        }
        .sheet(item: $store.scope(state: \.editSyncUp, action: \.editSyncUp)) { 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()
          }
        )
      }
    }
    EditingAndDeletingSyncUp-01-code-0008.swift
  9. Add the new cases to the Action enum that are being sent from the view.

    SyncUpDetail.swift
    import ComposableArchitecture
    import SwiftUI
    
    @Reducer
    struct SyncUpDetail {
      @ObservableState
      struct State: Equatable {
        @Presents var editSyncUp: SyncUpForm.State?
        @Shared var syncUp: SyncUp
      }
    
      enum Action {
        case cancelEditButtonTapped
        case deleteButtonTapped
        case doneEditingButtonTapped
        case editButtonTapped
        case editSyncUp(PresentationAction<SyncUpForm.Action>)
      }
    
      var body: some ReducerOf<Self> {
        Reduce { state, action in
          switch action {
          case .deleteButtonTapped:
            return .none
    
          case .editButtonTapped:
            state.editSyncUp = SyncUpForm.State(syncUp: state.syncUp)
            return .none
    
          case .editSyncUp:
            return .none
          }
        }
        .ifLet(\.$editSyncUp, action: \.editSyncUp) {
          SyncUpForm()
        }
      }
    }
    
    struct SyncUpDetailView: View {
      // ...
    }
    EditingAndDeletingSyncUp-01-code-0009.swift
  10. Implement the cancelEditButtonTapped action by simply clearing out the editSyncUp state. This will cause the view to dismiss the sheet.

    SyncUpDetail.swift
    import ComposableArchitecture
    import SwiftUI
    
    @Reducer
    struct SyncUpDetail {
      @ObservableState
      struct State: Equatable {
        @Presents var editSyncUp: SyncUpForm.State?
        @Shared var syncUp: SyncUp
      }
    
      enum Action {
        case cancelEditButtonTapped
        case deleteButtonTapped
        case doneEditingButtonTapped
        case editButtonTapped
        case editSyncUp(PresentationAction<SyncUpForm.Action>)
      }
    
      var body: some ReducerOf<Self> {
        Reduce { state, action in
          switch action {
          case .cancelEditButtonTapped:
            state.editSyncUp = nil
            return .none
    
          case .deleteButtonTapped:
            return .none
    
          case .editButtonTapped:
            state.editSyncUp = SyncUpForm.State(syncUp: state.syncUp)
            return .none
    
          case .editSyncUp:
            return .none
          }
        }
        .ifLet(\.$editSyncUp, action: \.editSyncUp) {
          SyncUpForm()
        }
      }
    }
    
    struct SyncUpDetailView: View {
      // ...
    }
    EditingAndDeletingSyncUp-01-code-0010.swift
  11. Implement the doneEditingButtonTapped action by grabbing the latest syncUp value from the editSyncUp state, and replacing SyncUpDetail’s state with that value.

    SyncUpDetail.swift
    import ComposableArchitecture
    import SwiftUI
    
    @Reducer
    struct SyncUpDetail {
      @ObservableState
      struct State: Equatable {
        @Presents var editSyncUp: SyncUpForm.State?
        @Shared var syncUp: SyncUp
      }
    
      enum Action {
        case cancelEditButtonTapped
        case deleteButtonTapped
        case doneEditingButtonTapped
        case editButtonTapped
        case editSyncUp(PresentationAction<SyncUpForm.Action>)
      }
    
      var body: some ReducerOf<Self> {
        Reduce { state, action in
          switch action {
          case .cancelEditButtonTapped:
            state.editSyncUp = nil
            return .none
    
          case .deleteButtonTapped:
            return .none
    
          case .doneEditingButtonTapped:
            guard let editedSyncUp = state.editSyncUp?.syncUp
            else { return .none }
            state.$syncUp.withLock { $0 = editedSyncUp }
            state.editSyncUp = nil
            return .none
    
          case .editButtonTapped:
            state.editSyncUp = SyncUpForm.State(syncUp: state.syncUp)
            return .none
    
          case .editSyncUp:
            return .none
          }
        }
        .ifLet(\.$editSyncUp, action: \.editSyncUp) {
          SyncUpForm()
        }
      }
    }
    
    struct SyncUpDetailView: View {
      // ...
    }
    EditingAndDeletingSyncUp-01-code-0011.swift
  12. Run the preview to see that you can tap the “Edit” button to bring up the sync-up form sheet. Then make some changes to the sync-up, and see that tapping “Done” causes those changes to be committed to the sync-up in the detail screen.

Deleting the sync-up

Next we will implement the logic for deleting the sync-up from the detail screen. Because deletion is performed by a simple button, we want to protect from accidental deletions by asking the user for confirmation using an alert. This will give us an opportunity to explore how one can show alerts in the Composable Architecture.

Technically, alerts can be shown in Composable Architecture features in exactly the same way they are in vanilla SwiftUI by using the various alert view modifiers. However, the library comes with some tools that can improve how alerts are shown, and can make the alerts more easily testable. We will use those tools.

Integrating the logic of an alert into a feature is a bit different from integrating the logic of a fully fledged feature. This is because alerts are immediately dismissed when a button is tapped and has no internal logic or behavior of its own. For this reason there is no reducer to specify in the trailing closure of ifLet, and in fact it can be removed.

We now have the alert integrated into the SyncUpDetail, but we aren’t yet populating the alert state to actually present the alert. This is done by constructing a value of AlertState, which is a data type that represents all of the properties of an alert, such as its title, message and buttons. Its primary purpose is to be Equatable-friendly so that alerts can be tested.

That’s all it takes to create AlertState. It contains all of the information for the view to display the alert, which we will do in a moment.

However, constructing AlertState values can be quite long, and so sometimes you may want to extract their creation to an extension on AlertState.

Next we need to handle the new alert actions in the reducer.

The .alert(.presented) case is more subtle, though. This action is sent when the user confirms that they want to delete the sync-up, and so somehow we have to remove the syncUp we have in state from the collection of sync-ups that is in the SyncUpsList feature.

However, remember that in Persisting sync-ups we showed how to persist the collection of sync-ups using the @Shared property wrapper with the fileStorage persistence strategy. This gives the app global access to that state, and we can make edits to it from anywhere. We can even do it directly inline in the .confirmButtonTapped action.

That’s all it takes to delete the sync-up from the persisted array of sync-ups. It may trouble you that we are reaching out to a seemingly global syncUps variable and mutating it. However, this is no different than making an API request to delete data on some external server. Typically for that situation we use dependencies to make the API operation testable, but we don’t need to do that with @Shared. It is testable by default.

If it truly bothers you to access the global syncUps state from within the detail feature, then you can instead send a “delegate” action from the detail that the parent feature intercepts and implements the deleting logic. That will allow the parent to handle deletion while the child remains ignorant to those details.

There is one more addition we want to make to the deletion functionality before moving on. When the user confirms deletion of the sync-up we should dismiss the detail view from being presented, as it’s no longer relevant. After all, the sync-up is being deleted!

This allows us to demonstrate another superpower of the Composable Architecture. The library comes with a powerful dependency called DismissEffect that allows a feature to dismiss itself if it is being presented, and if the feature was integrated into the parent using the library’s navigation tools. This can be powerful because it allows the child features to encapsulate as much of their logic as possible without having to explicitly communicate with the parent. It works similarly to SwiftUI’s @Environment(\.dismiss), but it is also distinct from it.

To dismiss a child feature with the DismissEffect dependency you just have to invoke it as if it’s a function: dismiss(). However, the dismiss effect is an asynchronous function, and therefore it cannot be invoked directly in the reducer. It can only be invoked inside an effect, and so we must use the run(priority:operation:catch:fileID:filePath:line:column:) effect, which gives us an asynchronous context to execute the work in, as well as a handle on Send for sending actions back into the system if needed.

Next we need to integrate the alert into the view layer so that an alert actually shows when the alert state becomes non-nil.

We have now finished the sync-up deletion functionality in the detail screen. Unfortunately we can’t yet test deletion because we still have no way to navigate to the detail screen from the sync-ups list screen.

But, before getting to that, let’s take a quick side excursion to clean up our domain modeling in this feature.

  1. We start by modeling a new Alert enum nested inside the Action enum that represents all the actions the user can take in the alert. The user can only confirm deletion or cancel deletion, and we do not have to model the cancel action. That is automatically taken care of by the library’s navigation tools.

    SyncUpDetail.swift
    import ComposableArchitecture
    import SwiftUI
    
    @Reducer
    struct SyncUpDetail {
      @ObservableState
      struct State: Equatable {
        @Presents var editSyncUp: SyncUpForm.State?
        @Shared var syncUp: SyncUp
      }
    
      enum Action {
        case cancelEditButtonTapped
        case deleteButtonTapped
        case doneEditingButtonTapped
        case editButtonTapped
        case editSyncUp(PresentationAction<SyncUpForm.Action>)
        @CasePathable
        enum Alert {
          case confirmButtonTapped
        }
      }
    
      var body: some ReducerOf<Self> {
        Reduce { state, action in
          switch action {
          case .cancelEditButtonTapped:
            state.editSyncUp = nil
            return .none
    
          case .deleteButtonTapped:
            return .none
    
          case .doneEditingButtonTapped:
            guard let editedSyncUp = state.editSyncUp?.syncUp
            else { return .none }
            state.$syncUp.withLock { $0 = editedSyncUp }
            state.editSyncUp = nil
            return .none
    
          case .editButtonTapped:
            state.editSyncUp = SyncUpForm.State(syncUp: state.syncUp)
            return .none
    
          case .editSyncUp:
            return .none
          }
        }
        .ifLet(\.$editSyncUp, action: \.editSyncUp) {
          SyncUpForm()
        }
      }
    }
    
    struct SyncUpDetailView: View {
      // ...
    }
    EditingAndDeletingSyncUp-02-code-0001.swift
  2. Add another piece of optional state using the Presents macro, this time representing whether or not an alert is shown.

    SyncUpDetail.swift
    import ComposableArchitecture
    import SwiftUI
    
    @Reducer
    struct SyncUpDetail {
      @ObservableState
      struct State: Equatable {
        @Presents var alert: AlertState<Action.Alert>?
        @Presents var editSyncUp: SyncUpForm.State?
        @Shared var syncUp: SyncUp
      }
    
      enum Action {
        case cancelEditButtonTapped
        case deleteButtonTapped
        case doneEditingButtonTapped
        case editButtonTapped
        case editSyncUp(PresentationAction<SyncUpForm.Action>)
        @CasePathable
        enum Alert {
          case confirmButtonTapped
        }
      }
    
      var body: some ReducerOf<Self> {
        Reduce { state, action in
          switch action {
          case .cancelEditButtonTapped:
            state.editSyncUp = nil
            return .none
    
          case .deleteButtonTapped:
            return .none
    
          case .doneEditingButtonTapped:
            guard let editedSyncUp = state.editSyncUp?.syncUp
            else { return .none }
            state.$syncUp.withLock { $0 = editedSyncUp }
            state.editSyncUp = nil
            return .none
    
          case .editButtonTapped:
            state.editSyncUp = SyncUpForm.State(syncUp: state.syncUp)
            return .none
    
          case .editSyncUp:
            return .none
          }
        }
        .ifLet(\.$editSyncUp, action: \.editSyncUp) {
          SyncUpForm()
        }
      }
    }
    
    struct SyncUpDetailView: View {
      // ...
    }
    EditingAndDeletingSyncUp-02-code-0002.swift
  3. Add another PresentationAction case to the Action enum that represents the actions the alert can send.

    SyncUpDetail.swift
    import ComposableArchitecture
    import SwiftUI
    
    @Reducer
    struct SyncUpDetail {
      @ObservableState
      struct State: Equatable {
        @Presents var alert: AlertState<Action.Alert>?
        @Presents var editSyncUp: SyncUpForm.State?
        @Shared var syncUp: SyncUp
      }
    
      enum Action {
        case alert(PresentationAction<Alert>)
        case cancelEditButtonTapped
        case deleteButtonTapped
        case doneEditingButtonTapped
        case editButtonTapped
        case editSyncUp(PresentationAction<SyncUpForm.Action>)
        @CasePathable
        enum Alert {
          case confirmButtonTapped
        }
      }
    
      var body: some ReducerOf<Self> {
        Reduce { state, action in
          switch action {
          case .cancelEditButtonTapped:
            state.editSyncUp = nil
            return .none
    
          case .deleteButtonTapped:
            return .none
    
          case .doneEditingButtonTapped:
            guard let editedSyncUp = state.editSyncUp?.syncUp
            else { return .none }
            state.$syncUp.withLock { $0 = editedSyncUp }
            state.editSyncUp = nil
            return .none
    
          case .editButtonTapped:
            state.editSyncUp = SyncUpForm.State(syncUp: state.syncUp)
            return .none
    
          case .editSyncUp:
            return .none
          }
        }
        .ifLet(\.$editSyncUp, action: \.editSyncUp) {
          SyncUpForm()
        }
      }
    }
    
    struct SyncUpDetailView: View {
      // ...
    }
    EditingAndDeletingSyncUp-02-code-0003.swift
  4. Handle the .alert case in the reducer, and use the ComposableArchitecture/Reducer/ifLet(_:action:then:fileID:line:)-7s8h2 operator again to integrate the alert’s logic into the SyncUpDetail reducer.

    SyncUpDetail.swift
    import ComposableArchitecture
    import SwiftUI
    
    @Reducer
    struct SyncUpDetail {
      @ObservableState
      struct State: Equatable {
        @Presents var alert: AlertState<Action.Alert>?
        @Presents var editSyncUp: SyncUpForm.State?
        @Shared var syncUp: SyncUp
      }
    
      enum Action {
        case alert(PresentationAction<Alert>)
        case cancelEditButtonTapped
        case deleteButtonTapped
        case doneEditingButtonTapped
        case editButtonTapped
        case editSyncUp(PresentationAction<SyncUpForm.Action>)
        @CasePathable
        enum Alert {
          case confirmButtonTapped
        }
      }
    
      var body: some ReducerOf<Self> {
        Reduce { state, action in
          switch action {
          case .alert:
            return .none
            
          case .cancelEditButtonTapped:
            state.editSyncUp = nil
            return .none
    
          case .deleteButtonTapped:
            return .none
    
          case .doneEditingButtonTapped:
            guard let editedSyncUp = state.editSyncUp?.syncUp
            else { return .none }
            state.$syncUp.withLock { $0 = editedSyncUp }
            state.editSyncUp = nil
            return .none
    
          case .editButtonTapped:
            state.editSyncUp = SyncUpForm.State(syncUp: state.syncUp)
            return .none
    
          case .editSyncUp:
            return .none
          }
        }
        .ifLet(\.$editSyncUp, action: \.editSyncUp) {
          SyncUpForm()
        }
        .ifLet(\.$alert, action: \.alert) {
          
        }
      }
    }
    
    struct SyncUpDetailView: View {
      // ...
    }
    EditingAndDeletingSyncUp-02-code-0004.swift
  5. Remove the trailing closure from the ifLet because the alert does not have any additional behavior to execute.

    SyncUpDetail.swift
    import ComposableArchitecture
    import SwiftUI
    
    @Reducer
    struct SyncUpDetail {
      @ObservableState
      struct State: Equatable {
        @Presents var alert: AlertState<Action.Alert>?
        @Presents var editSyncUp: SyncUpForm.State?
        @Shared var syncUp: SyncUp
      }
    
      enum Action {
        case alert(PresentationAction<Alert>)
        case cancelEditButtonTapped
        case deleteButtonTapped
        case doneEditingButtonTapped
        case editButtonTapped
        case editSyncUp(PresentationAction<SyncUpForm.Action>)
        @CasePathable
        enum Alert {
          case confirmButtonTapped
        }
      }
    
      var body: some ReducerOf<Self> {
        Reduce { state, action in
          switch action {
          case .alert:
            return .none
            
          case .cancelEditButtonTapped:
            state.editSyncUp = nil
            return .none
    
          case .deleteButtonTapped:
            return .none
    
          case .doneEditingButtonTapped:
            guard let editedSyncUp = state.editSyncUp?.syncUp
            else { return .none }
            state.$syncUp.withLock { $0 = editedSyncUp }
            state.editSyncUp = nil
            return .none
    
          case .editButtonTapped:
            state.editSyncUp = SyncUpForm.State(syncUp: state.syncUp)
            return .none
    
          case .editSyncUp:
            return .none
          }
        }
        .ifLet(\.$editSyncUp, action: \.editSyncUp) {
          SyncUpForm()
        }
        .ifLet(\.$alert, action: \.alert) 
      }
    }
    
    struct SyncUpDetailView: View {
      // ...
    }
    EditingAndDeletingSyncUp-02-code-0005.swift
  6. Populate the alert property by constructing AlertState. This is done by providing three trailing closures: one for the title, one for the buttons to show in the alert, and another for the message in the alert.

    SyncUpDetail.swift
    import ComposableArchitecture
    import SwiftUI
    
    @Reducer
    struct SyncUpDetail {
      @ObservableState
      struct State: Equatable {
        @Presents var alert: AlertState<Action.Alert>?
        @Presents var editSyncUp: SyncUpForm.State?
        @Shared var syncUp: SyncUp
      }
    
      enum Action {
        case alert(PresentationAction<Alert>)
        case cancelEditButtonTapped
        case deleteButtonTapped
        case doneEditingButtonTapped
        case editButtonTapped
        case editSyncUp(PresentationAction<SyncUpForm.Action>)
        @CasePathable
        enum Alert {
          case confirmButtonTapped
        }
      }
    
      var body: some ReducerOf<Self> {
        Reduce { state, action in
          switch action {
          case .alert:
            return .none
            
          case .cancelEditButtonTapped:
            state.editSyncUp = nil
            return .none
    
          case .deleteButtonTapped:
            state.alert = AlertState {
              TextState("Delete?")
            } actions: {
              ButtonState(role: .destructive, action: .confirmButtonTapped) {
                TextState("Yes")
              }
              ButtonState(role: .cancel) {
                TextState("Nevermind")
              }
            } message: {
              TextState("Are you sure you want to delete this meeting?")
            }
            return .none
    
          case .doneEditingButtonTapped:
            guard let editedSyncUp = state.editSyncUp?.syncUp
            else { return .none }
            state.$syncUp.withLock { $0 = editedSyncUp }
            state.editSyncUp = nil
            return .none
    
          case .editButtonTapped:
            state.editSyncUp = SyncUpForm.State(syncUp: state.syncUp)
            return .none
    
          case .editSyncUp:
            return .none
          }
        }
        .ifLet(\.$editSyncUp, action: \.editSyncUp) {
          SyncUpForm()
        }
        .ifLet(\.$alert, action: \.alert) 
      }
    }
    
    struct SyncUpDetailView: View {
      // ...
    }
    EditingAndDeletingSyncUp-02-code-0006.swift
  7. Extract the alert state to an extension on AlertState.

    SyncUpDetail.swift
    import ComposableArchitecture
    import SwiftUI
    
    @Reducer
    struct SyncUpDetail {
      @ObservableState
      struct State: Equatable {
        @Presents var alert: AlertState<Action.Alert>?
        @Presents var editSyncUp: SyncUpForm.State?
        @Shared var syncUp: SyncUp
      }
    
      enum Action {
        case alert(PresentationAction<Alert>)
        case cancelEditButtonTapped
        case deleteButtonTapped
        case doneEditingButtonTapped
        case editButtonTapped
        case editSyncUp(PresentationAction<SyncUpForm.Action>)
        @CasePathable
        enum Alert {
          case confirmButtonTapped
        }
      }
    
      var body: some ReducerOf<Self> {
        Reduce { state, action in
          switch action {
          case .alert:
            return .none
            
          case .cancelEditButtonTapped:
            state.editSyncUp = nil
            return .none
    
          case .deleteButtonTapped:
            state.alert = .deleteSyncUp
            return .none
    
          case .doneEditingButtonTapped:
            guard let editedSyncUp = state.editSyncUp?.syncUp
            else { return .none }
            state.$syncUp.withLock { $0 = editedSyncUp }
            state.editSyncUp = nil
            return .none
    
          case .editButtonTapped:
            state.editSyncUp = SyncUpForm.State(syncUp: state.syncUp)
            return .none
    
          case .editSyncUp:
            return .none
          }
        }
        .ifLet(\.$editSyncUp, action: \.editSyncUp) {
          SyncUpForm()
        }
        .ifLet(\.$alert, action: \.alert) 
      }
    }
    
    extension AlertState where Action == SyncUpDetail.Action.Alert {
      static let deleteSyncUp = Self {
        TextState("Delete?")
      } actions: {
        ButtonState(role: .destructive, action: .confirmButtonTapped) {
          TextState("Yes")
        }
        ButtonState(role: .cancel) {
          TextState("Nevermind")
        }
      } message: {
        TextState("Are you sure you want to delete this meeting?")
      }
    }
    
    struct SyncUpDetailView: View {
      // ...
    }
    EditingAndDeletingSyncUp-02-code-0007.swift
  8. Destructure the new .alert actions in the switch statement of the core Reduce.

    SyncUpDetail.swift
    import ComposableArchitecture
    import SwiftUI
    
    @Reducer
    struct SyncUpDetail {
      @ObservableState
      struct State: Equatable {
        @Presents var alert: AlertState<Action.Alert>?
        @Presents var editSyncUp: SyncUpForm.State?
        @Shared var syncUp: SyncUp
      }
    
      enum Action {
        case alert(PresentationAction<Alert>)
        case cancelEditButtonTapped
        case deleteButtonTapped
        case doneEditingButtonTapped
        case editButtonTapped
        case editSyncUp(PresentationAction<SyncUpForm.Action>)
        @CasePathable
        enum Alert {
          case confirmButtonTapped
        }
      }
    
      var body: some ReducerOf<Self> {
        Reduce { state, action in
          switch action {
          case .alert(.presented(.confirmButtonTapped)):
            
          case .alert(.dismiss):
    
          case .cancelEditButtonTapped:
            state.editSyncUp = nil
            return .none
    
          case .deleteButtonTapped:
            state.alert = .deleteSyncUp
            return .none
    
          case .doneEditingButtonTapped:
            guard let editedSyncUp = state.editSyncUp?.syncUp
            else { return .none }
            state.$syncUp.withLock { $0 = editedSyncUp }
            state.editSyncUp = nil
            return .none
    
          case .editButtonTapped:
            state.editSyncUp = SyncUpForm.State(syncUp: state.syncUp)
            return .none
    
          case .editSyncUp:
            return .none
          }
        }
        .ifLet(\.$editSyncUp, action: \.editSyncUp) {
          SyncUpForm()
        }
        .ifLet(\.$alert, action: \.alert) 
      }
    }
    
    extension AlertState where Action == SyncUpDetail.Action.Alert {
      static let deleteSyncUp = Self {
        TextState("Delete?")
      } actions: {
        ButtonState(role: .destructive, action: .confirmButtonTapped) {
          TextState("Yes")
        }
        ButtonState(role: .cancel) {
          TextState("Nevermind")
        }
      } message: {
        TextState("Are you sure you want to delete this meeting?")
      }
    }
    
    struct SyncUpDetailView: View {
      // ...
    }
    EditingAndDeletingSyncUp-02-code-0008.swift
  9. There is nothing to do in the .alert(.dismiss) case because the ifLet operator will automatically clean up the state for you.

    SyncUpDetail.swift
    import ComposableArchitecture
    import SwiftUI
    
    @Reducer
    struct SyncUpDetail {
      @ObservableState
      struct State: Equatable {
        @Presents var alert: AlertState<Action.Alert>?
        @Presents var editSyncUp: SyncUpForm.State?
        @Shared var syncUp: SyncUp
      }
    
      enum Action {
        case alert(PresentationAction<Alert>)
        case cancelEditButtonTapped
        case deleteButtonTapped
        case doneEditingButtonTapped
        case editButtonTapped
        case editSyncUp(PresentationAction<SyncUpForm.Action>)
        @CasePathable
        enum Alert {
          case confirmButtonTapped
        }
      }
    
      var body: some ReducerOf<Self> {
        Reduce { state, action in
          switch action {
          case .alert(.presented(.confirmButtonTapped)):
            
          case .alert(.dismiss):
            return .none
    
          case .cancelEditButtonTapped:
            state.editSyncUp = nil
            return .none
    
          case .deleteButtonTapped:
            state.alert = .deleteSyncUp
            return .none
    
          case .doneEditingButtonTapped:
            guard let editedSyncUp = state.editSyncUp?.syncUp
            else { return .none }
            state.$syncUp.withLock { $0 = editedSyncUp }
            state.editSyncUp = nil
            return .none
    
          case .editButtonTapped:
            state.editSyncUp = SyncUpForm.State(syncUp: state.syncUp)
            return .none
    
          case .editSyncUp:
            return .none
          }
        }
        .ifLet(\.$editSyncUp, action: \.editSyncUp) {
          SyncUpForm()
        }
        .ifLet(\.$alert, action: \.alert) 
      }
    }
    
    extension AlertState where Action == SyncUpDetail.Action.Alert {
      static let deleteSyncUp = Self {
        TextState("Delete?")
      } actions: {
        ButtonState(role: .destructive, action: .confirmButtonTapped) {
          TextState("Yes")
        }
        ButtonState(role: .cancel) {
          TextState("Nevermind")
        }
      } message: {
        TextState("Are you sure you want to delete this meeting?")
      }
    }
    
    struct SyncUpDetailView: View {
      // ...
    }
    EditingAndDeletingSyncUp-02-code-0009.swift
  10. Use the @Shared property wrapper with the .syncUps key to get a reference to the sync-ups loaded from disk.

    SyncUpDetail.swift
    import ComposableArchitecture
    import SwiftUI
    
    @Reducer
    struct SyncUpDetail {
      @ObservableState
      struct State: Equatable {
        @Presents var alert: AlertState<Action.Alert>?
        @Presents var editSyncUp: SyncUpForm.State?
        @Shared var syncUp: SyncUp
      }
    
      enum Action {
        case alert(PresentationAction<Alert>)
        case cancelEditButtonTapped
        case deleteButtonTapped
        case doneEditingButtonTapped
        case editButtonTapped
        case editSyncUp(PresentationAction<SyncUpForm.Action>)
        @CasePathable
        enum Alert {
          case confirmButtonTapped
        }
      }
    
      var body: some ReducerOf<Self> {
        Reduce { state, action in
          switch action {
          case .alert(.presented(.confirmButtonTapped)):
            @Shared(.fileStorage(.syncUps)) var syncUps: IdentifiedArrayOf<SyncUp> = []
    
          case .alert(.dismiss):
            return .none
    
          case .cancelEditButtonTapped:
            state.editSyncUp = nil
            return .none
    
          case .deleteButtonTapped:
            state.alert = .deleteSyncUp
            return .none
    
          case .doneEditingButtonTapped:
            guard let editedSyncUp = state.editSyncUp?.syncUp
            else { return .none }
            state.$syncUp.withLock { $0 = editedSyncUp }
            state.editSyncUp = nil
            return .none
    
          case .editButtonTapped:
            state.editSyncUp = SyncUpForm.State(syncUp: state.syncUp)
            return .none
    
          case .editSyncUp:
            return .none
          }
        }
        .ifLet(\.$editSyncUp, action: \.editSyncUp) {
          SyncUpForm()
        }
        .ifLet(\.$alert, action: \.alert) 
      }
    }
    
    extension AlertState where Action == SyncUpDetail.Action.Alert {
      static let deleteSyncUp = Self {
        TextState("Delete?")
      } actions: {
        ButtonState(role: .destructive, action: .confirmButtonTapped) {
          TextState("Yes")
        }
        ButtonState(role: .cancel) {
          TextState("Nevermind")
        }
      } message: {
        TextState("Are you sure you want to delete this meeting?")
      }
    }
    
    struct SyncUpDetailView: View {
      // ...
    }
    EditingAndDeletingSyncUp-02-code-0010.swift
  11. Use the id of the sync-up held in the detail’s state to remove it from the syncUps collection persisted to disk.

    SyncUpDetail.swift
    import ComposableArchitecture
    import SwiftUI
    
    @Reducer
    struct SyncUpDetail {
      @ObservableState
      struct State: Equatable {
        @Presents var alert: AlertState<Action.Alert>?
        @Presents var editSyncUp: SyncUpForm.State?
        @Shared var syncUp: SyncUp
      }
    
      enum Action {
        case alert(PresentationAction<Alert>)
        case cancelEditButtonTapped
        case deleteButtonTapped
        case doneEditingButtonTapped
        case editButtonTapped
        case editSyncUp(PresentationAction<SyncUpForm.Action>)
        @CasePathable
        enum Alert {
          case confirmButtonTapped
        }
      }
    
      var body: some ReducerOf<Self> {
        Reduce { state, action in
          switch action {
          case .alert(.presented(.confirmButtonTapped)):
            @Shared(.syncUps) var syncUps
            $syncUps.withLock { _ = $0.remove(id: state.syncUp.id) }
            return .none
    
          case .alert(.dismiss):
            return .none
    
          case .cancelEditButtonTapped:
            state.editSyncUp = nil
            return .none
    
          case .deleteButtonTapped:
            state.alert = .deleteSyncUp
            return .none
    
          case .doneEditingButtonTapped:
            guard let editedSyncUp = state.editSyncUp?.syncUp
            else { return .none }
            state.$syncUp.withLock { $0 = editedSyncUp }
            state.editSyncUp = nil
            return .none
    
          case .editButtonTapped:
            state.editSyncUp = SyncUpForm.State(syncUp: state.syncUp)
            return .none
    
          case .editSyncUp:
            return .none
          }
        }
        .ifLet(\.$editSyncUp, action: \.editSyncUp) {
          SyncUpForm()
        }
        .ifLet(\.$alert, action: \.alert) 
      }
    }
    
    extension AlertState where Action == SyncUpDetail.Action.Alert {
      static let deleteSyncUp = Self {
        TextState("Delete?")
      } actions: {
        ButtonState(role: .destructive, action: .confirmButtonTapped) {
          TextState("Yes")
        }
        ButtonState(role: .cancel) {
          TextState("Nevermind")
        }
      } message: {
        TextState("Are you sure you want to delete this meeting?")
      }
    }
    
    struct SyncUpDetailView: View {
      // ...
    }
    EditingAndDeletingSyncUp-02-code-0011.swift
  12. Add a dependency on DismissEffect to the SyncUpDetail feature by using the @Dependency property wrapper.

    SyncUpDetail.swift
    import ComposableArchitecture
    import SwiftUI
    
    @Reducer
    struct SyncUpDetail {
      @ObservableState
      struct State: Equatable {
        @Presents var alert: AlertState<Action.Alert>?
        @Presents var editSyncUp: SyncUpForm.State?
        @Shared var syncUp: SyncUp
      }
    
      enum Action {
        case alert(PresentationAction<Alert>)
        case cancelEditButtonTapped
        case deleteButtonTapped
        case doneEditingButtonTapped
        case editButtonTapped
        case editSyncUp(PresentationAction<SyncUpForm.Action>)
        @CasePathable
        enum Alert {
          case confirmButtonTapped
        }
      }
    
      @Dependency(\.dismiss) var dismiss
    
      var body: some ReducerOf<Self> {
        Reduce { state, action in
          switch action {
          case .alert(.presented(.confirmButtonTapped)):
            @Shared(.syncUps) var syncUps
            $syncUps.withLock { _ = $0.remove(id: state.syncUp.id) }
            return .none
    
          case .alert(.dismiss):
            return .none
    
          case .cancelEditButtonTapped:
            state.editSyncUp = nil
            return .none
    
          case .deleteButtonTapped:
            state.alert = .deleteSyncUp
            return .none
    
          case .doneEditingButtonTapped:
            guard let editedSyncUp = state.editSyncUp?.syncUp
            else { return .none }
            state.$syncUp.withLock { $0 = editedSyncUp }
            state.editSyncUp = nil
            return .none
    
          case .editButtonTapped:
            state.editSyncUp = SyncUpForm.State(syncUp: state.syncUp)
            return .none
    
          case .editSyncUp:
            return .none
          }
        }
        .ifLet(\.$editSyncUp, action: \.editSyncUp) {
          SyncUpForm()
        }
        .ifLet(\.$alert, action: \.alert) 
      }
    }
    
    extension AlertState where Action == SyncUpDetail.Action.Alert {
      static let deleteSyncUp = Self {
        TextState("Delete?")
      } actions: {
        ButtonState(role: .destructive, action: .confirmButtonTapped) {
          TextState("Yes")
        }
        ButtonState(role: .cancel) {
          TextState("Nevermind")
        }
      } message: {
        TextState("Are you sure you want to delete this meeting?")
      }
    }
    
    struct SyncUpDetailView: View {
      // ...
    }
    EditingAndDeletingSyncUp-02-code-0012.swift
  13. Return a .run effect to invoke the dismiss effect.

    SyncUpDetail.swift
    import ComposableArchitecture
    import SwiftUI
    
    @Reducer
    struct SyncUpDetail {
      @ObservableState
      struct State: Equatable {
        @Presents var alert: AlertState<Action.Alert>?
        @Presents var editSyncUp: SyncUpForm.State?
        @Shared var syncUp: SyncUp
      }
    
      enum Action {
        case alert(PresentationAction<Alert>)
        case cancelEditButtonTapped
        case deleteButtonTapped
        case doneEditingButtonTapped
        case editButtonTapped
        case editSyncUp(PresentationAction<SyncUpForm.Action>)
        @CasePathable
        enum Alert {
          case confirmButtonTapped
        }
      }
    
      @Dependency(\.dismiss) var dismiss
    
      var body: some ReducerOf<Self> {
        Reduce { state, action in
          switch action {
          case .alert(.presented(.confirmButtonTapped)):
            @Shared(.syncUps) var syncUps
            $syncUps.withLock { _ = $0.remove(id: state.syncUp.id) }
            return .run { _ in await dismiss() }
    
          case .alert(.dismiss):
            return .none
    
          case .cancelEditButtonTapped:
            state.editSyncUp = nil
            return .none
    
          case .deleteButtonTapped:
            state.alert = .deleteSyncUp
            return .none
    
          case .doneEditingButtonTapped:
            guard let editedSyncUp = state.editSyncUp?.syncUp
            else { return .none }
            state.$syncUp.withLock { $0 = editedSyncUp }
            state.editSyncUp = nil
            return .none
    
          case .editButtonTapped:
            state.editSyncUp = SyncUpForm.State(syncUp: state.syncUp)
            return .none
    
          case .editSyncUp:
            return .none
          }
        }
        .ifLet(\.$editSyncUp, action: \.editSyncUp) {
          SyncUpForm()
        }
        .ifLet(\.$alert, action: \.alert) 
      }
    }
    
    extension AlertState where Action == SyncUpDetail.Action.Alert {
      static let deleteSyncUp = Self {
        TextState("Delete?")
      } actions: {
        ButtonState(role: .destructive, action: .confirmButtonTapped) {
          TextState("Yes")
        }
        ButtonState(role: .cancel) {
          TextState("Nevermind")
        }
      } message: {
        TextState("Are you sure you want to delete this meeting?")
      }
    }
    
    struct SyncUpDetailView: View {
      // ...
    }
    EditingAndDeletingSyncUp-02-code-0013.swift
  14. In the SyncUpDetailView, go to the bottom and use the SwiftUI/View/alert(_:) view modifier that comes with the library. This will cause an alert to be presented when the alert state is populated.

    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
                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)
          }
        }
        .alert($store.scope(state: \.alert, action: \.alert))
        .sheet(item: $store.scope(state: \.editSyncUp, action: \.editSyncUp)) { 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()
          }
        )
      }
    }
    EditingAndDeletingSyncUp-02-code-0014.swift

More concise domain modeling

The SyncUpDetail works well enough right now, but there is something not ideal about how its domain is modeled. We are representing the alert and edit sheet using two independent optional values, which allows for invalid states. We only expect that either the alert or sheet is shown at one time, but it is possible for both optionals to be non-nil at the same time.

And this problem gets bigger with the most destinations you can navigate to from a feature. For example, if there are 4 possible destinations one can navigate to, then modeling that with optionals leads to 2^4 = 16 possible states, only 5 of which are valid. Either all optionals should be nil or exactly one should be non-nil.

This kind of inconcise domain modeling can leak complexity into your features, and luckily there is a better way.

That is all it takes to define a dedicated Destination reducer that encapsulates all of the logic and behavior of the features that can be navigated to. You can right click on the @Reducer macro and select “Expand macro” in Xcode to see all of the code that is being written for you.

Next we need to integrate this new Destination reducer into the main SyncUpDetail reducer.

That is all it takes to integrate the Destination reducer into the SyncUpDetail reducer. Next we have to update the core Reduce to handle the new destination state and actions properly.

That’s all it takes to fully integrate the Destination reducer into the SyncUpDetail reducer. Next we have to update the view so that we can drive the presentation of the alert and sheet from the destination enum.

That is all it takes to finish the refactor to use an enum to drive navigation rather than multiple optionals. Our domain is more concisely modeled, and we can know precisely when something is being presented. We merely have to check if destination != nil.

  1. Create a whole new reducer nested inside the SyncUpDetail called Destination. This reducer will represent all of the places one can navigate to from the detail screen, and that will give us a single piece of state to drive navigation rather than having multiple optional values.

    Further, the Destination type will be an enum. This is different than other reducers we have encountered so far. The @Reducer has special behavior when applied to enums that allows one to compose multiple reducers into a single package.

    SyncUpDetail.swift
    import ComposableArchitecture
    import SwiftUI
    
    @Reducer
    struct SyncUpDetail {
      @Reducer
      enum Destination {
      }
      // ...
    }
    extension SyncUpDetail.Destination.State: Equatable {}
    
    struct SyncUpDetailView: View {
      // ...
    }
    EditingAndDeletingSyncUp-03-code-0001.swift
  2. Add a case for each destination that can be navigated to from this feature. Each case will hold onto the reducer type of the feature being navigated to, except for the alert case. It will simply hold onto AlertState.

    We will also move the Alert action enum from the SyncUpDetail.Action type to be nested inside Destination.

    SyncUpDetail.swift
    import ComposableArchitecture
    import SwiftUI
    
    @Reducer
    struct SyncUpDetail {
      @Reducer
      enum Destination {
        case alert(AlertState<Alert>)
        case edit(SyncUpForm)
        @CasePathable
        enum Alert {
          case confirmButtonTapped
        }
      }
      // ...
    }
    extension SyncUpDetail.Destination.State: Equatable {}
    
    struct SyncUpDetailView: View {
      // ...
    }
    EditingAndDeletingSyncUp-03-code-0002.swift
  3. Replace the two independent pieces of optional state in SyncUpDetail.State for a single piece of optional Destination.State.

    We now have just one single piece of optional state that determines if navigation is active and where we are being navigated to.

    SyncUpDetail.swift
    import ComposableArchitecture
    import SwiftUI
    
    @Reducer
    struct SyncUpDetail {
      @Reducer
      enum Destination {
        case alert(AlertState<Alert>)
        case edit(SyncUpForm)
        @CasePathable
        enum Alert {
          case confirmButtonTapped
        }
      }
    
      @ObservableState
      struct State: Equatable {
        // @Presents var alert: AlertState<Action.Alert>?
        // @Presents var editSyncUp: SyncUpForm.State?
        @Presents var destination: Destination.State?
        @Shared var syncUp: SyncUp
      }
    
      enum Action {
        case alert(PresentationAction<Alert>)
        case cancelEditButtonTapped
        case deleteButtonTapped
        case doneEditingButtonTapped
        case editButtonTapped
        case editSyncUp(PresentationAction<SyncUpForm.Action>)
        @CasePathable
        enum Alert {
          case confirmButtonTapped
        }
      }
    
      @Dependency(\.dismiss) var dismiss
    
      var body: some ReducerOf<Self> {
        Reduce { state, action in
          switch action {
          case .alert(.presented(.confirmButtonTapped)):
            @Shared(.syncUps) var syncUps
            $syncUps.withLock { _ = $0.remove(id: state.syncUp.id) }
            return .run { _ in await dismiss() }
    
          case .alert(.dismiss):
            return .none
    
          case .cancelEditButtonTapped:
            state.editSyncUp = nil
            return .none
    
          case .delegate:
            return .none
    
          case .deleteButtonTapped:
            state.alert = .deleteSyncUp
            return .none
    
          case .doneEditingButtonTapped:
            guard let editedSyncUp = state.editSyncUp?.syncUp
            else { return .none }
            state.$syncUp.withLock { $0 = editedSyncUp }
            state.editSyncUp = nil
            return .none
    
          case .editButtonTapped:
            state.editSyncUp = SyncUpForm.State(syncUp: state.syncUp)
            return .none
    
          case .editSyncUp:
            return .none
          }
        }
        .ifLet(\.$editSyncUp, action: \.editSyncUp) {
          SyncUpForm()
        }
        .ifLet(\.$alert, action: \.alert) 
      }
    }
    extension SyncUpDetail.Destination.State: Equatable {}
    
    extension AlertState where Action == SyncUpDetail.Action.Alert {
      static let deleteSyncUp = Self {
        TextState("Delete?")
      } actions: {
        ButtonState(role: .destructive, action: .confirmButtonTapped) {
          TextState("Yes")
        }
        ButtonState(role: .cancel) {
          TextState("Nevermind")
        }
      } message: {
        TextState("Are you sure you want to delete this meeting?")
      }
    }
    
    struct SyncUpDetailView: View {
      // ...
    }
    EditingAndDeletingSyncUp-03-code-0003.swift
  4. Replace the two action cases for the alert and edit sheet with a single case that holds on to a PresentationAction of Destination.Action.

    SyncUpDetail.swift
    import ComposableArchitecture
    import SwiftUI
    
    @Reducer
    struct SyncUpDetail {
      @Reducer
      enum Destination {
        case alert(AlertState<Alert>)
        case edit(SyncUpForm)
        @CasePathable
        enum Alert {
          case confirmButtonTapped
        }
      }
    
      @ObservableState
      struct State: Equatable {
        @Presents var destination: Destination.State?
        @Shared var syncUp: SyncUp
      }
    
      enum Action {
        // case alert(PresentationAction<Alert>)
        case cancelEditButtonTapped
        case deleteButtonTapped
        case destination(PresentationAction<Destination.Action>)
        case doneEditingButtonTapped
        case editButtonTapped
        // case editSyncUp(PresentationAction<SyncUpForm.Action>)
        // enum Alert {
        //   case confirmButtonTapped
        // }
      }
    
      @Dependency(\.dismiss) var dismiss
    
      var body: some ReducerOf<Self> {
        Reduce { state, action in
          switch action {
          case .alert(.presented(.confirmButtonTapped)):
            @Shared(.syncUps) var syncUps
            $syncUps.withLock { _ = $0.remove(id: state.syncUp.id) }
            return .run { _ in await dismiss() }
    
          case .alert(.dismiss):
            return .none
    
          case .cancelEditButtonTapped:
            state.editSyncUp = nil
            return .none
    
          case .delegate:
            return .none
    
          case .deleteButtonTapped:
            state.alert = .deleteSyncUp
            return .none
    
          case .doneEditingButtonTapped:
            guard let editedSyncUp = state.editSyncUp?.syncUp
            else { return .none }
            state.$syncUp.withLock { $0 = editedSyncUp }
            state.editSyncUp = nil
            return .none
    
          case .editButtonTapped:
            state.editSyncUp = SyncUpForm.State(syncUp: state.syncUp)
            return .none
    
          case .editSyncUp:
            return .none
          }
        }
        .ifLet(\.$editSyncUp, action: \.editSyncUp) {
          SyncUpForm()
        }
        .ifLet(\.$alert, action: \.alert) 
      }
    }
    extension SyncUpDetail.Destination.State: Equatable {}
    
    extension AlertState where Action == SyncUpDetail.Action.Alert {
      static let deleteSyncUp = Self {
        TextState("Delete?")
      } actions: {
        ButtonState(role: .destructive, action: .confirmButtonTapped) {
          TextState("Yes")
        }
        ButtonState(role: .cancel) {
          TextState("Nevermind")
        }
      } message: {
        TextState("Are you sure you want to delete this meeting?")
      }
    }
    
    struct SyncUpDetailView: View {
      // ...
    }
    EditingAndDeletingSyncUp-03-code-0004.swift
  5. Replace the two ifLet operators at the bottom of SyncUpDetail with a single one that composes the Destination reducer.

    SyncUpDetail.swift
    import ComposableArchitecture
    import SwiftUI
    
    @Reducer
    struct SyncUpDetail {
      @Reducer
      enum Destination {
        case alert(AlertState<Alert>)
        case edit(SyncUpForm)
        @CasePathable
        enum Alert {
          case confirmButtonTapped
        }
      }
    
      @ObservableState
      struct State: Equatable {
        @Presents var destination: Destination.State?
        @Shared var syncUp: SyncUp
      }
    
      enum Action {
        case cancelEditButtonTapped
        case deleteButtonTapped
        case destination(PresentationAction<Destination.Action>)
        case doneEditingButtonTapped
        case editButtonTapped
      }
    
      @Dependency(\.dismiss) var dismiss
    
      var body: some ReducerOf<Self> {
        Reduce { state, action in
          switch action {
          case .alert(.presented(.confirmButtonTapped)):
            @Shared(.syncUps) var syncUps
            $syncUps.withLock { _ = $0.remove(id: state.syncUp.id) }
            return .run { _ in await dismiss() }
    
          case .alert(.dismiss):
            return .none
    
          case .cancelEditButtonTapped:
            state.editSyncUp = nil
            return .none
    
          case .delegate:
            return .none
    
          case .deleteButtonTapped:
            state.alert = .deleteSyncUp
            return .none
    
          case .destination:
            return .none
    
          case .doneEditingButtonTapped:
            guard let editedSyncUp = state.editSyncUp?.syncUp
            else { return .none }
            state.$syncUp.withLock { $0 = editedSyncUp }
            state.editSyncUp = nil
            return .none
    
          case .editButtonTapped:
            state.editSyncUp = SyncUpForm.State(syncUp: state.syncUp)
            return .none
          }
        }
        // .ifLet(\.$editSyncUp, action: \.editSyncUp) {
        //   SyncUpForm()
        // }
        // .ifLet(\.$alert, action: \.alert)
        .ifLet(\.$destination, action: \.destination)
      }
    }
    extension SyncUpDetail.Destination.State: Equatable {}
    
    extension AlertState where Action == SyncUpDetail.Action.Alert {
      static let deleteSyncUp = Self {
        TextState("Delete?")
      } actions: {
        ButtonState(role: .destructive, action: .confirmButtonTapped) {
          TextState("Yes")
        }
        ButtonState(role: .cancel) {
          TextState("Nevermind")
        }
      } message: {
        TextState("Are you sure you want to delete this meeting?")
      }
    }
    
    struct SyncUpDetailView: View {
      // ...
    }
    EditingAndDeletingSyncUp-03-code-0005.swift
  6. Update the pattern matching for the alert to go through the .destination(.presented) case.

    SyncUpDetail.swift
    import ComposableArchitecture
    import SwiftUI
    
    @Reducer
    struct SyncUpDetail {
      @Reducer
      enum Destination {
        case alert(AlertState<Alert>)
        case edit(SyncUpForm)
        @CasePathable
        enum Alert {
          case confirmButtonTapped
        }
      }
    
      @ObservableState
      struct State: Equatable {
        @Presents var destination: Destination.State?
        @Shared var syncUp: SyncUp
      }
    
      enum Action {
        case cancelEditButtonTapped
        case deleteButtonTapped
        case destination(PresentationAction<Destination.Action>)
        case doneEditingButtonTapped
        case editButtonTapped
      }
    
      @Dependency(\.dismiss) var dismiss
    
      var body: some ReducerOf<Self> {
        Reduce { state, action in
          switch action {
          // case .alert(.presented(.confirmButtonTapped)):
          case .destination(.presented(.alert(.confirmButtonTapped))):
            @Shared(.syncUps) var syncUps
            $syncUps.withLock { _ = $0.remove(id: state.syncUp.id) }
            return .run { _ in await dismiss() }
    
          case .alert(.dismiss):
            return .none
    
          case .cancelEditButtonTapped:
            state.editSyncUp = nil
            return .none
    
          case .delegate:
            return .none
    
          case .deleteButtonTapped:
            state.alert = .deleteSyncUp
            return .none
    
          case .destination:
            return .none
    
          case .doneEditingButtonTapped:
            guard let editedSyncUp = state.editSyncUp?.syncUp
            else { return .none }
            state.$syncUp.withLock { $0 = editedSyncUp }
            state.editSyncUp = nil
            return .none
    
          case .editButtonTapped:
            state.editSyncUp = SyncUpForm.State(syncUp: state.syncUp)
            return .none
          }
        }
        .ifLet(\.$destination, action: \.destination)
      }
    }
    extension SyncUpDetail.Destination.State: Equatable {}
    
    extension AlertState where Action == SyncUpDetail.Action.Alert {
      static let deleteSyncUp = Self {
        TextState("Delete?")
      } actions: {
        ButtonState(role: .destructive, action: .confirmButtonTapped) {
          TextState("Yes")
        }
        ButtonState(role: .cancel) {
          TextState("Nevermind")
        }
      } message: {
        TextState("Are you sure you want to delete this meeting?")
      }
    }
    
    struct SyncUpDetailView: View {
      // ...
    }
    EditingAndDeletingSyncUp-03-code-0006.swift
  7. Return .none for all other .destination actions.

    SyncUpDetail.swift
    import ComposableArchitecture
    import SwiftUI
    
    @Reducer
    struct SyncUpDetail {
      @Reducer
      enum Destination {
        case alert(AlertState<Alert>)
        case edit(SyncUpForm)
        @CasePathable
        enum Alert {
          case confirmButtonTapped
        }
      }
    
      @ObservableState
      struct State: Equatable {
        @Presents var destination: Destination.State?
        @Shared var syncUp: SyncUp
      }
    
      enum Action {
        case cancelEditButtonTapped
        case deleteButtonTapped
        case destination(PresentationAction<Destination.Action>)
        case doneEditingButtonTapped
        case editButtonTapped
      }
    
      @Dependency(\.dismiss) var dismiss
    
      var body: some ReducerOf<Self> {
        Reduce { state, action in
          switch action {
          // case .alert(.presented(.confirmButtonTapped)):
          case .destination(.presented(.alert(.confirmButtonTapped))):
            @Shared(.syncUps) var syncUps
            $syncUps.withLock { _ = $0.remove(id: state.syncUp.id) }
            return .run { _ in await dismiss() }
    
          case .destination:
            return .none
    
          case .cancelEditButtonTapped:
            state.editSyncUp = nil
            return .none
    
          case .delegate:
            return .none
    
          case .deleteButtonTapped:
            state.alert = .deleteSyncUp
            return .none
    
          case .doneEditingButtonTapped:
            guard let editedSyncUp = state.editSyncUp?.syncUp
            else { return .none }
            state.$syncUp.withLock { $0 = editedSyncUp }
            state.editSyncUp = nil
            return .none
    
          case .editButtonTapped:
            state.editSyncUp = SyncUpForm.State(syncUp: state.syncUp)
            return .none
          }
        }
        .ifLet(\.$destination, action: \.destination)
      }
    }
    extension SyncUpDetail.Destination.State: Equatable {}
    
    extension AlertState where Action == SyncUpDetail.Action.Alert {
      static let deleteSyncUp = Self {
        TextState("Delete?")
      } actions: {
        ButtonState(role: .destructive, action: .confirmButtonTapped) {
          TextState("Yes")
        }
        ButtonState(role: .cancel) {
          TextState("Nevermind")
        }
      } message: {
        TextState("Are you sure you want to delete this meeting?")
      }
    }
    
    struct SyncUpDetailView: View {
      // ...
    }
    EditingAndDeletingSyncUp-03-code-0007.swift
  8. Update where we nil out editSyncUp state to instead nil out the destination state.

    SyncUpDetail.swift
    import ComposableArchitecture
    import SwiftUI
    
    @Reducer
    struct SyncUpDetail {
      @Reducer
      enum Destination {
        case alert(AlertState<Alert>)
        case edit(SyncUpForm)
        @CasePathable
        enum Alert {
          case confirmButtonTapped
        }
      }
    
      @ObservableState
      struct State: Equatable {
        @Presents var destination: Destination.State?
        @Shared var syncUp: SyncUp
      }
    
      enum Action {
        case cancelEditButtonTapped
        case deleteButtonTapped
        case destination(PresentationAction<Destination.Action>)
        case doneEditingButtonTapped
        case editButtonTapped
      }
    
      @Dependency(\.dismiss) var dismiss
    
      var body: some ReducerOf<Self> {
        Reduce { state, action in
          switch action {
          // case .alert(.presented(.confirmButtonTapped)):
          case .destination(.presented(.alert(.confirmButtonTapped))):
            @Shared(.syncUps) var syncUps
            $syncUps.withLock { _ = $0.remove(id: state.syncUp.id) }
            return .run { _ in await dismiss() }
    
          case .destination:
            return .none
    
          case .cancelEditButtonTapped:
            state.destination = nil
            return .none
    
          case .delegate:
            return .none
    
          case .deleteButtonTapped:
            state.alert = .deleteSyncUp
            return .none
    
          case .doneEditingButtonTapped:
            guard let editedSyncUp = state.editSyncUp?.syncUp
            else { return .none }
            state.$syncUp.withLock { $0 = editedSyncUp }
            state.editSyncUp = nil
            return .none
    
          case .editButtonTapped:
            state.editSyncUp = SyncUpForm.State(syncUp: state.syncUp)
            return .none
          }
        }
        .ifLet(\.$destination, action: \.destination)
      }
    }
    extension SyncUpDetail.Destination.State: Equatable {}
    
    extension AlertState where Action == SyncUpDetail.Action.Alert {
      static let deleteSyncUp = Self {
        TextState("Delete?")
      } actions: {
        ButtonState(role: .destructive, action: .confirmButtonTapped) {
          TextState("Yes")
        }
        ButtonState(role: .cancel) {
          TextState("Nevermind")
        }
      } message: {
        TextState("Are you sure you want to delete this meeting?")
      }
    }
    
    struct SyncUpDetailView: View {
      // ...
    }
    EditingAndDeletingSyncUp-03-code-0008.swift
  9. Update where we populate the alert state to instead point the destination state to the .alert case.

    SyncUpDetail.swift
    import ComposableArchitecture
    import SwiftUI
    
    @Reducer
    struct SyncUpDetail {
      @Reducer
      enum Destination {
        case alert(AlertState<Alert>)
        case edit(SyncUpForm)
        @CasePathable
        enum Alert {
          case confirmButtonTapped
        }
      }
    
      @ObservableState
      struct State: Equatable {
        @Presents var destination: Destination.State?
        @Shared var syncUp: SyncUp
      }
    
      enum Action {
        case cancelEditButtonTapped
        case deleteButtonTapped
        case destination(PresentationAction<Destination.Action>)
        case doneEditingButtonTapped
        case editButtonTapped
      }
    
      @Dependency(\.dismiss) var dismiss
    
      var body: some ReducerOf<Self> {
        Reduce { state, action in
          switch action {
          // case .alert(.presented(.confirmButtonTapped)):
          case .destination(.presented(.alert(.confirmButtonTapped))):
            @Shared(.syncUps) var syncUps
            $syncUps.withLock { _ = $0.remove(id: state.syncUp.id) }
            return .run { _ in await dismiss() }
    
          case .destination:
            return .none
    
          case .cancelEditButtonTapped:
            state.destination = nil
            return .none
    
          case .deleteButtonTapped:
            // state.alert = .deleteSyncUp
            state.destination = .alert(.deleteSyncUp)
            return .none
    
          case .doneEditingButtonTapped:
            guard let editedSyncUp = state.editSyncUp?.syncUp
            else { return .none }
            state.$syncUp.withLock { $0 = editedSyncUp }
            state.editSyncUp = nil
            return .none
    
          case .editButtonTapped:
            state.editSyncUp = SyncUpForm.State(syncUp: state.syncUp)
            return .none
          }
        }
        .ifLet(\.$destination, action: \.destination)
      }
    }
    extension SyncUpDetail.Destination.State: Equatable {}
    
    extension AlertState where Action == SyncUpDetail.Action.Alert {
      static let deleteSyncUp = Self {
        TextState("Delete?")
      } actions: {
        ButtonState(role: .destructive, action: .confirmButtonTapped) {
          TextState("Yes")
        }
        ButtonState(role: .cancel) {
          TextState("Nevermind")
        }
      } message: {
        TextState("Are you sure you want to delete this meeting?")
      }
    }
    
    struct SyncUpDetailView: View {
      // ...
    }
    EditingAndDeletingSyncUp-03-code-0009.swift
  10. Update the .doneEditingButtonTapped action by grabbing the edited sync-up from the .edit case of the destination enum.

    SyncUpDetail.swift
    import ComposableArchitecture
    import SwiftUI
    
    @Reducer
    struct SyncUpDetail {
      @Reducer
      enum Destination {
        case alert(AlertState<Alert>)
        case edit(SyncUpForm)
        @CasePathable
        enum Alert {
          case confirmButtonTapped
        }
      }
    
      @ObservableState
      struct State: Equatable {
        @Presents var destination: Destination.State?
        @Shared var syncUp: SyncUp
      }
    
      enum Action {
        case cancelEditButtonTapped
        case deleteButtonTapped
        case destination(PresentationAction<Destination.Action>)
        case doneEditingButtonTapped
        case editButtonTapped
      }
    
      @Dependency(\.dismiss) var dismiss
    
      var body: some ReducerOf<Self> {
        Reduce { state, action in
          switch action {
          // case .alert(.presented(.confirmButtonTapped)):
          case .destination(.presented(.alert(.confirmButtonTapped))):
            @Shared(.syncUps) var syncUps
            $syncUps.withLock { _ = $0.remove(id: state.syncUp.id) }
            return .run { _ in await dismiss() }
    
          case .destination:
            return .none
    
          case .cancelEditButtonTapped:
            state.destination = nil
            return .none
    
          case .deleteButtonTapped:
            state.destination = .alert(.deleteSyncUp)
            return .none
    
          case .doneEditingButtonTapped:
            guard let editedSyncUp = state.destination?.edit?.syncUp
            else { return .none }
            state.$syncUp.withLock { $0 = editedSyncUp }
            state.destination = nil
            return .none
    
          case .editButtonTapped:
            state.editSyncUp = SyncUpForm.State(syncUp: state.syncUp)
            return .none
          }
        }
        .ifLet(\.$destination, action: \.destination)
      }
    }
    extension SyncUpDetail.Destination.State: Equatable {}
    
    extension AlertState where Action == SyncUpDetail.Action.Alert {
      static let deleteSyncUp = Self {
        TextState("Delete?")
      } actions: {
        ButtonState(role: .destructive, action: .confirmButtonTapped) {
          TextState("Yes")
        }
        ButtonState(role: .cancel) {
          TextState("Nevermind")
        }
      } message: {
        TextState("Are you sure you want to delete this meeting?")
      }
    }
    
    struct SyncUpDetailView: View {
      // ...
    }
    EditingAndDeletingSyncUp-03-code-0010.swift
  11. Update where we previously populated the editSyncUp state to instead point the destination enum to the .edit case with some SyncUpForm state.

    SyncUpDetail.swift
    import ComposableArchitecture
    import SwiftUI
    
    @Reducer
    struct SyncUpDetail {
      @Reducer
      enum Destination {
        case alert(AlertState<Alert>)
        case edit(SyncUpForm)
        @CasePathable
        enum Alert {
          case confirmButtonTapped
        }
      }
    
      @ObservableState
      struct State: Equatable {
        @Presents var destination: Destination.State?
        @Shared var syncUp: SyncUp
      }
    
      enum Action {
        case cancelEditButtonTapped
        case deleteButtonTapped
        case destination(PresentationAction<Destination.Action>)
        case doneEditingButtonTapped
        case editButtonTapped
      }
    
      @Dependency(\.dismiss) var dismiss
    
      var body: some ReducerOf<Self> {
        Reduce { state, action in
          switch action {
          // case .alert(.presented(.confirmButtonTapped)):
          case .destination(.presented(.alert(.confirmButtonTapped))):
            @Shared(.syncUps) var syncUps
            $syncUps.withLock { _ = $0.remove(id: state.syncUp.id) }
            return .run { _ in await dismiss() }
    
          case .destination:
            return .none
    
          case .cancelEditButtonTapped:
            state.destination = nil
            return .none
    
          case .deleteButtonTapped:
            state.destination = .alert(.deleteSyncUp)
            return .none
    
          case .doneEditingButtonTapped:
            guard let editedSyncUp = state.destination?.edit?.syncUp
            else { return .none }
            state.$syncUp.withLock { $0 = editedSyncUp }
            state.destination = nil
            return .none
    
          case .editButtonTapped:
            // state.editSyncUp = SyncUpForm.State(syncUp: state.syncUp)
            state.destination = .edit(SyncUpForm.State(syncUp: state.syncUp))
            return .none
          }
        }
        .ifLet(\.$destination, action: \.destination)
      }
    }
    extension SyncUpDetail.Destination.State: Equatable {}
    
    extension AlertState where Action == SyncUpDetail.Action.Alert {
      static let deleteSyncUp = Self {
        TextState("Delete?")
      } actions: {
        ButtonState(role: .destructive, action: .confirmButtonTapped) {
          TextState("Yes")
        }
        ButtonState(role: .cancel) {
          TextState("Nevermind")
        }
      } message: {
        TextState("Are you sure you want to delete this meeting?")
      }
    }
    
    struct SyncUpDetailView: View {
      // ...
    }
    EditingAndDeletingSyncUp-03-code-0011.swift
  12. Update the extension on AlertState to point to the Destination’s alert action.

    SyncUpDetail.swift
    import ComposableArchitecture
    import SwiftUI
    
    @Reducer
    struct SyncUpDetail {
      @Reducer
      enum Destination {
        case alert(AlertState<Alert>)
        case edit(SyncUpForm)
        @CasePathable
        enum Alert {
          case confirmButtonTapped
        }
      }
    
      @ObservableState
      struct State: Equatable {
        @Presents var destination: Destination.State?
        @Shared var syncUp: SyncUp
      }
    
      enum Action {
        case cancelEditButtonTapped
        case deleteButtonTapped
        case destination(PresentationAction<Destination.Action>)
        case doneEditingButtonTapped
        case editButtonTapped
      }
    
      @Dependency(\.dismiss) var dismiss
    
      var body: some ReducerOf<Self> {
        Reduce { state, action in
          switch action {
          // case .alert(.presented(.confirmButtonTapped)):
          case .destination(.presented(.alert(.confirmButtonTapped))):
            @Shared(.syncUps) var syncUps
            $syncUps.withLock { _ = $0.remove(id: state.syncUp.id) }
            return .run { _ in await dismiss() }
    
          case .destination:
            return .none
    
          case .cancelEditButtonTapped:
            state.destination = nil
            return .none
    
          case .deleteButtonTapped:
            state.destination = .alert(.deleteSyncUp)
            return .none
    
          case .doneEditingButtonTapped:
            guard let editedSyncUp = state.destination?.edit?.syncUp
            else { return .none }
            state.$syncUp.withLock { $0 = editedSyncUp }
            state.destination = nil
            return .none
    
          case .editButtonTapped:
            // state.editSyncUp = SyncUpForm.State(syncUp: state.syncUp)
            state.destination = .edit(SyncUpForm.State(syncUp: state.syncUp))
            return .none
          }
        }
        .ifLet(\.$destination, action: \.destination)
      }
    }
    extension SyncUpDetail.Destination.State: Equatable {}
    
    extension AlertState where Action == SyncUpDetail.Destination.Alert {
      static let deleteSyncUp = Self {
        TextState("Delete?")
      } actions: {
        ButtonState(role: .destructive, action: .confirmButtonTapped) {
          TextState("Yes")
        }
        ButtonState(role: .cancel) {
          TextState("Nevermind")
        }
      } message: {
        TextState("Are you sure you want to delete this meeting?")
      }
    }
    
    struct SyncUpDetailView: View {
      // ...
    }
    EditingAndDeletingSyncUp-03-code-0012.swift
  13. Update the SwiftUI/View/alert(_:) and .sheet(item:) modifiers at the bottom of the view so that the $store.scope further singles out the case for driving navigation.

    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
                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)
          }
        }
        .toolbar {
          Button("Edit") {
            store.send(.editButtonTapped)
          }
        }
        .navigationTitle(Text(store.syncUp.title))
        .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()
          }
        )
      }
    }
    EditingAndDeletingSyncUp-03-code-0013.swift