Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Updating a SwiftUI App to Use the Composable Ar...

Updating a SwiftUI App to Use the Composable Architecture

Updating a SwiftUI App to Use the Composable Architecture by Yu-Che Cheng @ COSCUP 2024
https://coscup.org/2024/zh-TW/session/NYMPBQ

Avatar for iamhands0me

iamhands0me

August 03, 2024
Tweet

More Decks by iamhands0me

Other Decks in Programming

Transcript

  1. 👨💻 View Action State SwiftUI Render User Interaction Mutation Updates

    Make from the WWDC19: Data Flow Through SwiftUI https://developer.apple.com/videos/play/wwdc2019/226/
  2. } } struct ScrumsView: View { @Binding var scrums: [DailyScrum]

    @Environment(\.scenePhase) private var scenePhase @State private var isPresentingNewScrumView = false let saveAction: () -> Void var body: some View { NavigationStack { List($scrums) { $scrum in NavigationLink(destination: DetailView(scrum: $scrum)) { CardView(scrum: scrum) } .listRowBackground(scrum.theme.mainColor) } .navigationTitle("Daily Scrums") .toolbar { Button(action: { isPresentingNewScrumView = true }) { Image(systemName: "plus") } } } .sheet(isPresented: $isPresentingNewScrumView) { NewScrumSheet(scrums: $scrums, isPresentingNewScrumView: $isPresentingNewScrumView) } .onChange(of: scenePhase) { phase in if phase == .inactive { saveAction() } } } } #Preview { ScrumsView(scrums: .constant(DailyScrum.sampleData), saveAction: {})
  3. @Reducer struct ScrumsFeature { @ObservableState struct State: Equatable { }

    enum Action { } } struct ScrumsView: View { @Binding var scrums: [DailyScrum] @Environment(\.scenePhase) private var scenePhase @State private var isPresentingNewScrumView = false let saveAction: () -> Void var body: some View { NavigationStack { List($scrums) { $scrum in NavigationLink(destination: DetailView(scrum: $scrum)) { CardView(scrum: scrum) } .listRowBackground(scrum.theme.mainColor) } .navigationTitle("Daily Scrums") .toolbar { Button(action: { isPresentingNewScrumView = true }) { Image(systemName: "plus") } } }
  4. @Reducer struct ScrumsFeature { @ObservableState struct State: Equatable { var

    scrums: [DailyScrum] = [] // TODO: Pass from outside var isPresentingNewScrumView = false } enum Action { } } struct ScrumsView: View { @Environment(\.scenePhase) private var scenePhase let saveAction: () -> Void var body: some View { NavigationStack { List($scrums) { $scrum in NavigationLink(destination: DetailView(scrum: $scrum)) { CardView(scrum: scrum) } .listRowBackground(scrum.theme.mainColor) } .navigationTitle("Daily Scrums") .toolbar { Button(action: { isPresentingNewScrumView = true }) { Image(systemName: "plus") } }
  5. @Reducer struct ScrumsFeature { @ObservableState struct State: Equatable { var

    scrums: [DailyScrum] = [] // TODO: Pass from outside var isPresentingNewScrumView = false } enum Action { case newScrumButtonTapped } var body: some ReducerOf<Self> { Reduce { state, action in switch action { case .newScrumButtonTapped: state.isPresentingNewScrumView = true return .none } } } struct ScrumsView: View { @Environment(\.scenePhase) private var scenePhase let saveAction: () -> Void var body: some View { NavigationStack { List($scrums) { $scrum in NavigationLink(destination: DetailView(scrum: $scrum)) { CardView(scrum: scrum) } .listRowBackground(scrum.theme.mainColor) } .navigationTitle("Daily Scrums") .toolbar { Button(action: { isPresentingNewScrumView = true }) { Image(systemName: "plus") } }
  6. @Reducer struct ScrumsFeature { @ObservableState struct State: Equatable { var

    scrums: [DailyScrum] = [] // TODO: Pass from outside var isPresentingNewScrumView = false } enum Action { case newScrumButtonTapped } var body: some ReducerOf<Self> { Reduce { state, action in switch action { case .newScrumButtonTapped: state.isPresentingNewScrumView = true return .none } } } struct ScrumsView: View { @Environment(\.scenePhase) private var scenePhase let saveAction: () -> Void var body: some View { NavigationStack { List($scrums) { $scrum in NavigationLink(destination: DetailView(scrum: $scrum)) { CardView(scrum: scrum) } .listRowBackground(scrum.theme.mainColor) } .navigationTitle("Daily Scrums") .toolbar { Button(action: {
  7. @Reducer struct ScrumsFeature { @ObservableState struct State: Equatable { var

    scrums: [DailyScrum] = [] // TODO: Pass from outside var isPresentingNewScrumView = false } enum Action { case newScrumButtonTapped } var body: some ReducerOf<Self> { Reduce { state, action in switch action { case .newScrumButtonTapped: state.isPresentingNewScrumView = true return .none } } } struct ScrumsView: View { @Environment(\.scenePhase) private var scenePhase let saveAction: () -> Void var body: some View { NavigationStack { List($scrums) { $scrum in NavigationLink(destination: DetailView(scrum: $scrum)) { CardView(scrum: scrum) } .listRowBackground(scrum.theme.mainColor)
  8. } } struct ScrumsView: View { let store: StoreOf<ScrumsFeature> @Environment(\.scenePhase)

    private var scenePhase let saveAction: () -> Void var body: some View { NavigationStack { List($scrums) { $scrum in NavigationLink(destination: DetailView(scrum: $scrum)) { CardView(scrum: scrum) } .listRowBackground(scrum.theme.mainColor) } .navigationTitle("Daily Scrums") .toolbar { Button(action: { isPresentingNewScrumView = true }) { Image(systemName: "plus") } } } .sheet(isPresented: $isPresentingNewScrumView) { NewScrumSheet(scrums: $scrums, isPresentingNewScrumView: $isPresentingNewScrumView) } .onChange(of: scenePhase) { phase in if phase == .inactive { saveAction() } } } } #Preview {
  9. } } struct ScrumsView: View { @Bindable var store: StoreOf<ScrumsFeature>

    @Environment(\.scenePhase) private var scenePhase let saveAction: () -> Void var body: some View { NavigationStack { List($scrums) { $scrum in NavigationLink(destination: DetailView(scrum: $scrum)) { CardView(scrum: scrum) } .listRowBackground(scrum.theme.mainColor) } .navigationTitle("Daily Scrums") .toolbar { Button(action: { isPresentingNewScrumView = true }) { Image(systemName: "plus") } } } .sheet(isPresented: $isPresentingNewScrumView) { NewScrumSheet(scrums: $scrums, isPresentingNewScrumView: $isPresentingNewScrumView) } .onChange(of: scenePhase) { phase in if phase == .inactive { saveAction() } } } } #Preview {
  10. } } struct ScrumsView: View { @Bindable var store: StoreOf<ScrumsFeature>

    @Environment(\.scenePhase) private var scenePhase let saveAction: () -> Void var body: some View { NavigationStack { List($stroe.scrums) { $scrum in NavigationLink(destination: DetailView(scrum: $scrum)) { CardView(scrum: scrum) } .listRowBackground(scrum.theme.mainColor) } .navigationTitle("Daily Scrums") .toolbar { Button(action: { isPresentingNewScrumView = true }) { Image(systemName: "plus") } } } .sheet(isPresented: $stroe.isPresentingNewScrumView) { NewScrumSheet(scrums: $store.scrums, isPresentingNewScrumView: $store.isPresentingNewScrumView) } .onChange(of: scenePhase) { phase in if phase == .inactive { saveAction() } } } } #Preview {
  11. } } struct ScrumsView: View { @Bindable var store: StoreOf<ScrumsFeature>

    @Environment(\.scenePhase) private var scenePhase let saveAction: () -> Void var body: some View { NavigationStack { List($stroe.scrums) { $scrum in NavigationLink(destination: DetailView(scrum: $scrum)) { CardView(scrum: scrum) } .listRowBackground(scrum.theme.mainColor) } .navigationTitle("Daily Scrums") .toolbar { Button(action: { store.send(.newScrumButtonTapped) }) { Image(systemName: "plus") } } } .sheet(isPresented: $stroe.isPresentingNewScrumView) { NewScrumSheet(scrums: $store.scrums, isPresentingNewScrumView: $store.isPresentingNewScrumView) } .onChange(of: scenePhase) { phase in if phase == .inactive { saveAction() } } } } #Preview {
  12. @Reducer struct ScrumsFeature { @ObservableState struct State: Equatable { var

    scrums: [DailyScrum] = [] // TODO: Pass from outside var isPresentingNewScrumView = false } enum Action { case newScrumButtonTapped case binding(BindingAction<State>) } var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .newScrumButtonTapped: state.isPresentingNewScrumView = true return .none case .binding: return .none } } } } struct ScrumsView: View { @Bindable var store: StoreOf<ScrumsFeature> @Environment(\.scenePhase) private var scenePhase let saveAction: () -> Void var body: some View { NavigationStack { List($stroe.scrums) { $scrum in NavigationLink(destination: DetailView(scrum: $scrum)) { CardView(scrum: scrum) }
  13. @Reducer struct ScrumsFeature { @ObservableState struct State: Equatable { var

    scrums: [DailyScrum] = [] // TODO: Pass from outside var isPresentingNewScrumView = false } enum Action: BindableAction { case newScrumButtonTapped case binding(BindingAction<State>) } var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .newScrumButtonTapped: state.isPresentingNewScrumView = true return .none case .binding: return .none } } } } struct ScrumsView: View { @Bindable var store: StoreOf<ScrumsFeature> @Environment(\.scenePhase) private var scenePhase let saveAction: () -> Void var body: some View { NavigationStack { List($stroe.scrums) { $scrum in NavigationLink(destination: DetailView(scrum: $scrum)) { CardView(scrum: scrum) }
  14. @Reducer struct ScrumsFeature { @ObservableState struct State: Equatable { var

    scrums: [DailyScrum] = [] // TODO: Pass from outside var isPresentingNewScrumView = false } enum Action: BindableAction { case newScrumButtonTapped case binding(BindingAction<State>) } var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .newScrumButtonTapped: state.isPresentingNewScrumView = true return .none case .binding: return .none } } } } struct ScrumsView: View { @Bindable var store: StoreOf<ScrumsFeature> @Environment(\.scenePhase) private var scenePhase let saveAction: () -> Void var body: some View { NavigationStack { List($stroe.scrums) { $scrum in NavigationLink(destination: DetailView(scrum: $scrum)) { CardView(scrum: scrum)
  15. @Reducer struct ScrumsFeature { @ObservableState struct State: Equatable { var

    scrums: [DailyScrum] = [] // TODO: Pass from outside var isPresentingNewScrumView = false } enum Action: BindableAction { case newScrumButtonTapped case binding(BindingAction<State>) } var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .newScrumButtonTapped: state.isPresentingNewScrumView = true return .none case .binding: return .none } } } } struct ScrumsView: View { @Bindable var store: StoreOf<ScrumsFeature> @Environment(\.scenePhase) private var scenePhase let saveAction: () -> Void var body: some View { NavigationStack { List($stroe.scrums) { $scrum in
  16. @Reducer struct ScrumsFeature { @ObservableState struct State: Equatable { var

    scrums: [DailyScrum] = [] // TODO: Pass from outside var isPresentingNewScrumView = false } enum Action: BindableAction { case newScrumButtonTapped case binding(BindingAction<State>) } var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .newScrumButtonTapped: state.isPresentingNewScrumView = true return .none case .binding: return .none } } } } struct ScrumsView: View { @Bindable var store: StoreOf<ScrumsFeature> @Environment(\.scenePhase) private var scenePhase let saveAction: () -> Void var body: some View { NavigationStack {
  17. store.send(.newScrumButtonTapped) }) { Image(systemName: "plus") } } } .sheet(isPresented: $stroe.isPresentingNewScrumView)

    { NewScrumSheet(scrums: $store.scrums, isPresentingNewScrumView: $store.isPresentingNewScrumView) } .onChange(of: scenePhase) { phase in if phase == .inactive { saveAction() } } } } #Preview { ScrumsView( scrums: .constant(DailyScrum.sampleData), saveAction: {} ) }
  18. store.send(.newScrumButtonTapped) }) { Image(systemName: "plus") } } } .sheet(isPresented: $stroe.isPresentingNewScrumView)

    { NewScrumSheet(scrums: $store.scrums, isPresentingNewScrumView: $store.isPresentingNewScrumView) } .onChange(of: scenePhase) { phase in if phase == .inactive { saveAction() } } } } #Preview { ScrumsView( store: Store( initialState: Reducer.State , reducer: () -> Reducer, ), saveAction: {} ) }
  19. store.send(.newScrumButtonTapped) }) { Image(systemName: "plus") } } } .sheet(isPresented: $stroe.isPresentingNewScrumView)

    { NewScrumSheet(scrums: $store.scrums, isPresentingNewScrumView: $store.isPresentingNewScrumView) } .onChange(of: scenePhase) { phase in if phase == .inactive { saveAction() } } } } #Preview { ScrumsView( store: Store( initialState: ScrumsFeature.State(scrums: DailyScrum.sampleData), reducer: () -> Reducer, ), saveAction: {} ) }
  20. store.send(.newScrumButtonTapped) }) { Image(systemName: "plus") } } } .sheet(isPresented: $stroe.isPresentingNewScrumView)

    { NewScrumSheet(scrums: $store.scrums, isPresentingNewScrumView: $store.isPresentingNewScrumView) } .onChange(of: scenePhase) { phase in if phase == .inactive { saveAction() } } } } #Preview { ScrumsView( store: Store( initialState: ScrumsFeature.State(scrums: DailyScrum.sampleData), reducer: { ScrumsFeature() } ), saveAction: {} ) }
  21. store.send(.newScrumButtonTapped) }) { Image(systemName: "plus") } } } .sheet(isPresented: $stroe.isPresentingNewScrumView)

    { NewScrumSheet(scrums: $store.scrums, isPresentingNewScrumView: $store.isPresentingNewScrumView) } .onChange(of: scenePhase) { phase in if phase == .inactive { saveAction() } } } } #Preview { ScrumsView( store: Store(initialState: ScrumsFeature.State(scrums: DailyScrum.sampleData)) { ScrumsFeature() }, saveAction: {} ) }
  22. } } struct DetailView: View { @Binding var scrum: DailyScrum

    @State private var editingScrum = DailyScrum.emptyScrum @State private var isPresentingEditView = false var body: some View { List { /* ... */ } .navigationTitle(scrum.title) .toolbar { Button("Edit") { isPresentingEditView = true editingScrum = scrum } } .sheet(isPresented: $isPresentingEditView) { NavigationStack { DetailEditView(scrum: $editingScrum) .navigationTitle(scrum.title) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { isPresentingEditView = false } } ToolbarItem(placement: .confirmationAction) { Button("Done") { isPresentingEditView = false scrum = editingScrum } } } } }
  23. @Reducer struct DetailFeature { @ObservableState struct State: Equatable { false

    } enum Action { case editButtonTapped case cancelEditButtonTapped case confirmEditButtonTapped case binding(BindingAction<State>) } var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .editButtonTapped: state.isPresentingEditView = true state.editingScrum = state.scrum return .none case .cancelEditButtonTapped: state.isPresentingEditView = false return .none case .confirmEditButtonTapped: state.isPresentingEditView = false state.scrum = state.editingScrum return .none case .binding: return .none } } } } struct DetailView: View { @Binding var scrum: DailyScrum @State private var editingScrum = DailyScrum.emptyScrum @State private var isPresentingEditView = false var body: some View { List { /* ... */ } .navigationTitle(scrum.title) .toolbar { Button("Edit") { isPresentingEditView = true editingScrum = scrum } } .sheet(isPresented: $isPresentingEditView) {
  24. @Reducer struct DetailFeature { @ObservableState struct State: Equatable { @Shared

    var scrum: DailyScrum var editingScrum = DailyScrum.emptyScrum var isPresentingEditView = false } enum Action { case editButtonTapped case cancelEditButtonTapped case confirmEditButtonTapped case binding(BindingAction<State>) } var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .editButtonTapped: state.isPresentingEditView = true state.editingScrum = state.scrum return .none case .cancelEditButtonTapped: state.isPresentingEditView = false return .none case .confirmEditButtonTapped: state.isPresentingEditView = false state.scrum = state.editingScrum return .none case .binding: return .none } } } } struct DetailView: View { var body: some View { List { /* ... */ } .navigationTitle(scrum.title) .toolbar { Button("Edit") { isPresentingEditView = true editingScrum = scrum } } .sheet(isPresented: $isPresentingEditView) {
  25. @Reducer struct DetailFeature { @ObservableState struct State: Equatable { @Shared

    var scrum: DailyScrum var editingScrum = DailyScrum.emptyScrum var isPresentingEditView = false } enum Action: BindableAction { case editButtonTapped case cancelEditButtonTapped case confirmEditButtonTapped case binding(BindingAction<State>) } var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .editButtonTapped: state.isPresentingEditView = true state.editingScrum = state.scrum return .none case .cancelEditButtonTapped: state.isPresentingEditView = false return .none case .confirmEditButtonTapped: state.isPresentingEditView = false state.scrum = state.editingScrum return .none case .binding: return .none } } } } struct DetailView: View { var body: some View { List { /* ... */ } .navigationTitle(scrum.title) .toolbar { Button("Edit") { isPresentingEditView = true editingScrum = scrum
  26. @Reducer struct DetailFeature { /* ... */ @ObservableState struct State:

    Equatable { @Shared var scrum: DailyScrum var editingScrum = DailyScrum.emptyScrum var isPresentingEditView = false } enum Action: BindableAction { case editButtonTapped case cancelEditButtonTapped case confirmEditButtonTapped case binding(BindingAction<State>) } var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .editButtonTapped: state.isPresentingEditView = true state.editingScrum = state.scrum return .none case .cancelEditButtonTapped: state.isPresentingEditView = false return .none case .confirmEditButtonTapped: state.isPresentingEditView = false state.scrum = state.editingScrum return .none case .binding: return .none } } } } struct DetailView: View { var body: some View { List { /* ... */ } .navigationTitle(scrum.title) .toolbar { Button("Edit") { isPresentingEditView = true editingScrum = scrum } } .sheet(isPresented: $isPresentingEditView) { NavigationStack { DetailEditView(scrum: $editingScrum) .navigationTitle(scrum.title) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { isPresentingEditView = false } }
  27. @Reducer struct DetailFeature { /* ... */ @ObservableState struct State:

    Equatable { @Shared var scrum: DailyScrum var editingScrum = DailyScrum.emptyScrum var isPresentingEditView = false } enum Action: BindableAction { case editButtonTapped case cancelEditButtonTapped case confirmEditButtonTapped case binding(BindingAction<State>) } var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .editButtonTapped: state.isPresentingEditView = true state.editingScrum = state.scrum return .none case .cancelEditButtonTapped: state.isPresentingEditView = false return .none case .confirmEditButtonTapped: state.isPresentingEditView = false state.scrum = state.editingScrum return .none case .binding: return .none } } } } struct DetailView: View { var body: some View { List { /* ... */ } .navigationTitle(scrum.title) .toolbar { Button("Edit") { isPresentingEditView = true editingScrum = scrum } } .sheet(isPresented: $isPresentingEditView) {
  28. @Reducer struct DetailFeature { /* ... */ @ObservableState struct State:

    Equatable { @Shared var scrum: DailyScrum var editingScrum = DailyScrum.emptyScrum var isPresentingEditView = false } enum Action: BindableAction { case editButtonTapped case cancelEditButtonTapped case confirmEditButtonTapped case binding(BindingAction<State>) } var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .editButtonTapped: state.isPresentingEditView = true state.editingScrum = state.scrum return .none case .cancelEditButtonTapped: state.isPresentingEditView = false return .none case .confirmEditButtonTapped: state.isPresentingEditView = false state.scrum = state.editingScrum return .none case .binding: return .none } } } } struct DetailView: View { var body: some View { List { /* ... */ } .navigationTitle(scrum.title) .toolbar {
  29. } } struct DetailView: View { var body: some View

    { List { /* ... */ } .navigationTitle(scrum.title) .toolbar { Button("Edit") { } } .sheet(isPresented: $isPresentingEditView) { NavigationStack { DetailEditView(scrum: $editingScrum) .navigationTitle(scrum.title) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { } } ToolbarItem(placement: .confirmationAction) { Button("Done") { } } } } } } }
  30. } } struct DetailView: View { @Bindable var store: StoreOf<DetailFeature>

    var body: some View { List { /* ... */ } .navigationTitle(scrum.title) .toolbar { Button("Edit") { } } .sheet(isPresented: $isPresentingEditView) { NavigationStack { DetailEditView(scrum: $editingScrum) .navigationTitle(scrum.title) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { } } ToolbarItem(placement: .confirmationAction) { Button("Done") { } } } } } } }
  31. } } struct DetailView: View { @Bindable var store: StoreOf<DetailFeature>

    var body: some View { List { /* ... */ } .navigationTitle(store.scrum.title) .toolbar { Button("Edit") { } } .sheet(isPresented: $store.isPresentingEditView) { NavigationStack { DetailEditView(scrum: $store.editingScrum) .navigationTitle(store.scrum.title) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { } } ToolbarItem(placement: .confirmationAction) { Button("Done") { } } } } } } }
  32. } } struct DetailView: View { @Bindable var store: StoreOf<DetailFeature>

    var body: some View { List { /* ... */ } .navigationTitle(store.scrum.title) .toolbar { Button("Edit") { store.send(.editButtonTapped) } } .sheet(isPresented: $store.isPresentingEditView) { NavigationStack { DetailEditView(scrum: $store.editingScrum) .navigationTitle(store.scrum.title) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { store.send(.cancelEditButtonTapped) } } ToolbarItem(placement: .confirmationAction) { Button("Done") { store.send(.confirmEditButtonTapped) } } } } } } } #Preview {
  33. } struct ScrumsView: View { @Bindable var store: StoreOf<ScrumsFeature> @Environment(\.scenePhase)

    private var scenePhase let saveAction: () -> Void var body: some View { NavigationStack { List($stroe.scrums) { $scrum in NavigationLink(destination: DetailView(scrum: $scrum)) { CardView(scrum: scrum) } .listRowBackground(scrum.theme.mainColor) } /* ... */ } destination: { store in DetailView(store: store) } .sheet(isPresented: $stroe.isPresentingNewScrumView) { NewScrumSheet(scrums: $store.scrums, isPresentingNewScrumView: $store.isPresentingNewScrumView) } /* ... */ } } #Preview { ScrumsView( store: Store(initialState: ScrumsFeature.State(scrums: DailyScrum.sampleData)) { ScrumsFeature() }, saveAction: {} ) } DetailView(scrum: $scrum)
  34. @Reducer struct ScrumsFeature { @ObservableState struct State: Equatable { var

    scrums: [DailyScrum] = [] // TODO: Pass from outside @Shared var scrums: IdentifiedArrayOf<DailyScrum> var isPresentingNewScrumView = false var path = StackState<DetailFeature.State>() } enum Action: BindableAction { case newScrumButtonTapped case binding(BindingAction<State>) case path(StackAction<DetailFeature.State, DetailFeature.Action>) } var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .newScrumButtonTapped: state.isPresentingNewScrumView = true return .none case .binding: return .none case .path: return .none } } .forEach(\.path, action: \.path) { DetailFeature() } } } struct ScrumsView: View { @Bindable var store: StoreOf<ScrumsFeature> @Environment(\.scenePhase) private var scenePhase let saveAction: () -> Void var body: some View { NavigationStack {
  35. @Reducer struct ScrumsFeature { @ObservableState struct State: Equatable { @Shared

    var scrums: IdentifiedArrayOf<DailyScrum> var isPresentingNewScrumView = false var path = StackState<DetailFeature.State>() } enum Action: BindableAction { case newScrumButtonTapped case binding(BindingAction<State>) case path(StackAction<DetailFeature.State, DetailFeature.Action>) } var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .newScrumButtonTapped: state.isPresentingNewScrumView = true return .none case .binding: return .none case .path: return .none } } .forEach(\.path, action: \.path) { DetailFeature() } } } struct ScrumsView: View { @Bindable var store: StoreOf<ScrumsFeature> @Environment(\.scenePhase) private var scenePhase let saveAction: () -> Void var body: some View { NavigationStack {
  36. @Reducer struct ScrumsFeature { @ObservableState struct State: Equatable { @Shared

    var scrums: IdentifiedArrayOf<DailyScrum> var isPresentingNewScrumView = false var path = StackState<DetailFeature.State>() } enum Action: BindableAction { case newScrumButtonTapped case binding(BindingAction<State>) case path(StackAction<DetailFeature.State, DetailFeature.Action>) } var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .newScrumButtonTapped: state.isPresentingNewScrumView = true return .none case .binding: return .none case .path: return .none } } .forEach(\.path, action: \.path) { DetailFeature() } } } struct ScrumsView: View { @Bindable var store: StoreOf<ScrumsFeature> @Environment(\.scenePhase) private var scenePhase let saveAction: () -> Void var body: some View {
  37. @Reducer struct ScrumsFeature { @ObservableState struct State: Equatable { @Shared

    var scrums: IdentifiedArrayOf<DailyScrum> var isPresentingNewScrumView = false var path = StackState<DetailFeature.State>() } enum Action: BindableAction { case newScrumButtonTapped case binding(BindingAction<State>) case path(StackAction<DetailFeature.State, DetailFeature.Action>) } var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .newScrumButtonTapped: state.isPresentingNewScrumView = true return .none case .binding: return .none case .path: return .none } } .forEach(\.path, action: \.path) { DetailFeature() } } } struct ScrumsView: View { @Bindable var store: StoreOf<ScrumsFeature> @Environment(\.scenePhase) private var scenePhase let saveAction: () -> Void
  38. @Reducer struct ScrumsFeature { @ObservableState struct State: Equatable { @Shared

    var scrums: IdentifiedArrayOf<DailyScrum> var isPresentingNewScrumView = false var path = StackState<DetailFeature.State>() } enum Action: BindableAction { case newScrumButtonTapped case binding(BindingAction<State>) case path(StackAction<DetailFeature.State, DetailFeature.Action>) } var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .newScrumButtonTapped: state.isPresentingNewScrumView = true return .none case .binding: return .none case .path: return .none } } .forEach(\.path, action: \.path) { DetailFeature() } } } struct ScrumsView: View { @Bindable var store: StoreOf<ScrumsFeature> @Environment(\.scenePhase) private var scenePhase
  39. @Reducer struct ScrumsFeature { @ObservableState struct State: Equatable { @Shared

    var scrums: IdentifiedArrayOf<DailyScrum> var isPresentingNewScrumView = false var path = StackState<DetailFeature.State>() } enum Action: BindableAction { case newScrumButtonTapped case binding(BindingAction<State>) case path(StackAction<DetailFeature.State, DetailFeature.Action>) } var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .newScrumButtonTapped: state.isPresentingNewScrumView = true return .none case .binding: return .none case .path: return .none } } .forEach(\.path, action: \.path) { DetailFeature() } } } struct ScrumsView: View {
  40. } } struct ScrumsView: View { @Bindable var store: StoreOf<ScrumsFeature>

    @Environment(\.scenePhase) private var scenePhase let saveAction: () -> Void var body: some View { NavigationStack { List($stroe.scrums) { $scrum in NavigationLink(destination: DetailView(scrum: $scrum)) { CardView(scrum: scrum) } .listRowBackground(scrum.theme.mainColor) } /* ... */ } destination: { store in DetailView(store: store) } .sheet(isPresented: $stroe.isPresentingNewScrumView) { NewScrumSheet(scrums: $store.scrums, isPresentingNewScrumView: $store.isPresentingNewScrumView) } /* ... */ } } #Preview { ScrumsView( store: Store(initialState: ScrumsFeature.State(scrums: DailyScrum.sampleData)) { ScrumsFeature() }, saveAction: {} ) }
  41. } } struct ScrumsView: View { @Bindable var store: StoreOf<ScrumsFeature>

    @Environment(\.scenePhase) private var scenePhase let saveAction: () -> Void var body: some View { NavigationStack(path: $store.scope(state: \.path, action: \.path)) { List($stroe.scrums) { $scrum in NavigationLink(destination: DetailView(scrum: $scrum)) { CardView(scrum: scrum) } .listRowBackground(scrum.theme.mainColor) } /* ... */ } destination: { store in DetailView(store: store) } .sheet(isPresented: $stroe.isPresentingNewScrumView) { NewScrumSheet(scrums: $store.scrums, isPresentingNewScrumView: $store.isPresentingNewScrumView) } /* ... */ } } #Preview { ScrumsView( store: Store(initialState: ScrumsFeature.State(scrums: DailyScrum.sampleData)) { ScrumsFeature() }, saveAction: {} ) }
  42. } } struct ScrumsView: View { @Bindable var store: StoreOf<ScrumsFeature>

    @Environment(\.scenePhase) private var scenePhase let saveAction: () -> Void var body: some View { NavigationStack(path: $store.scope(state: \.path, action: \.path)) { List($stroe.scrums) { $scrum in NavigationLink(destination: DetailView(scrum: $scrum)) { CardView(scrum: scrum) } .listRowBackground(scrum.theme.mainColor) } /* ... */ } destination: { store in DetailView(store: store) } .sheet(isPresented: $stroe.isPresentingNewScrumView) { NewScrumSheet(scrums: $store.scrums, isPresentingNewScrumView: $store.isPresentingNewScrumView) } /* ... */ } } #Preview { ScrumsView( store: Store(initialState: ScrumsFeature.State(scrums: DailyScrum.sampleData)) { ScrumsFeature() }, saveAction: {} ) }
  43. } } struct ScrumsView: View { @Bindable var store: StoreOf<ScrumsFeature>

    @Environment(\.scenePhase) private var scenePhase let saveAction: () -> Void var body: some View { NavigationStack(path: $store.scope(state: \.path, action: \.path)) { List($stroe.scrums) { $scrum in NavigationLink(state: DetailFeature.State(scrum: $scrum)) { CardView(scrum: scrum) } .listRowBackground(scrum.theme.mainColor) } /* ... */ } destination: { store in DetailView(store: store) } .sheet(isPresented: $stroe.isPresentingNewScrumView) { NewScrumSheet(scrums: $store.scrums, isPresentingNewScrumView: $store.isPresentingNewScrumView) } /* ... */ } } #Preview { ScrumsView( store: Store(initialState: ScrumsFeature.State(scrums: DailyScrum.sampleData)) { ScrumsFeature() }, saveAction: {} ) }
  44. } } struct ScrumsView: View { @Bindable var store: StoreOf<ScrumsFeature>

    @Environment(\.scenePhase) private var scenePhase let saveAction: () -> Void var body: some View { NavigationStack(path: $store.scope(state: \.path, action: \.path)) { List(store.$scrums.elements) { $scrum in NavigationLink(state: DetailFeature.State(scrum: $scrum)) { CardView(scrum: scrum) } .listRowBackground(scrum.theme.mainColor) } /* ... */ } destination: { store in DetailView(store: store) } .sheet(isPresented: $stroe.isPresentingNewScrumView) { NewScrumSheet(scrums: $store.scrums, isPresentingNewScrumView: $store.isPresentingNewScrumView) } /* ... */ } } #Preview { ScrumsView( store: Store(initialState: ScrumsFeature.State(scrums: DailyScrum.sampleData)) { ScrumsFeature() }, saveAction: {} ) }
  45. } } struct ScrumsView: View { @Bindable var store: StoreOf<ScrumsFeature>

    @Environment(\.scenePhase) private var scenePhase let saveAction: () -> Void var body: some View { NavigationStack(path: $store.scope(state: \.path, action: \.path)) { List(store.$scrums.elements) { $scrum in NavigationLink(state: DetailFeature.State(scrum: $scrum)) { CardView(scrum: scrum) } .listRowBackground(scrum.theme.mainColor) } /* ... */ } destination: { store in DetailView(store: store) } .sheet(isPresented: $stroe.isPresentingNewScrumView) { // TODO: Use TCA’s IdentifiedArray in NewScrumSheet NewScrumSheet(scrums: $store.scrums, isPresentingNewScrumView: $store.isPresentingNewScrumView) } /* ... */ } } #Preview { ScrumsView( store: Store(initialState: ScrumsFeature.State(scrums: DailyScrum.sampleData)) { ScrumsFeature() }, saveAction: {} ) }
  46. } } struct ScrumdingerApp: App { @StateObject private var store

    = ScrumStore() @State private var errorWrapper: ErrorWrapper? var body: some Scene { WindowGroup { ScrumsView(scrums: $store.scrums) { Task { do { try await store.save(scrums: store.scrums) } catch { errorWrapper = ErrorWrapper(error: error, guidance: "Try again later.") } } } .task { do { try await store.load() } catch { errorWrapper = ErrorWrapper( error: error, guidance: "Scrumdinger will load sample data and continue.” ) } } .sheet(item: $errorWrapper) { store.scrums = DailyScrum.sampleData } content: { wrapper in ErrorView(errorWrapper: wrapper) } } } }
  47. @Reducer struct AppFeature { @ObservableState struct State: Equatable { var

    scrumList = ScrumsFeature.State(scrums: Shared(IdentifiedArray(uniqueElements: []))) var errorWrapper: ErrorWrapper? } enum Action: BindableAction { case scrumList(ScrumsFeature.Action) case onSave case onAppear case errorSheetDismissed case binding(BindingAction<State>) case onError(ErrorWrapper) case onLoaded([DailyScrum]) } var body: some ReducerOf<Self> { BindingReducer() Scope(state: \.scrumList, action: \.scrumList) { ScrumsFeature() } Reduce { state, action in switch action { case .scrumList: return .none case .onSave: return .run { [scrums = state.scrumList.scrums] send in do {try await save(scrums: scrums.elements) } catch { await send(.onError(ErrorWrapper(error: error, /* ... */))) } } case .onAppear: return .run { send in do {try await send(.onLoaded(load())) } catch { await send(.onError(ErrorWrapper(error: error, guidance: /* ... */))) } } case .errorSheetDismissed: state.scrumList.scrums = IdentifiedArray(uniqueElements: DailyScrum.sampleData) return .none case .binding: return .none case let .onError(errorWrapper): state.errorWrapper = errorWrapper return .none case let .onLoaded(scrums): state.scrumList.scrums = IdentifiedArray(uniqueElements: scrums) return .none } } } } struct ScrumdingerApp: App { @StateObject private var store = ScrumStore() @State private var errorWrapper: ErrorWrapper? var body: some Scene { WindowGroup { ScrumsView(scrums: $store.scrums) { Task { do { try await store.save(scrums: store.scrums) } catch { errorWrapper = ErrorWrapper(error: error, guidance: "Try again later.") } }
  48. @Reducer struct AppFeature { @ObservableState struct State: Equatable { /*

    ... */ var scrumList = ScrumsFeature.State(scrums: Shared(IdentifiedArray(uniqueElements: []))) var errorWrapper: ErrorWrapper? } enum Action: BindableAction { /* ... */ case scrumList(ScrumsFeature.Action) case onSave case onAppear case errorSheetDismissed case binding(BindingAction<State>) case onError(ErrorWrapper) case onLoaded([DailyScrum]) } var body: some ReducerOf<Self> { BindingReducer() Scope(state: \.scrumList, action: \.scrumList) { ScrumsFeature() } Reduce { state, action in switch action { case .scrumList: return .none case .onSave: return .run { [scrums = state.scrumList.scrums] send in do {try await save(scrums: scrums.elements) } catch { await send(.onError(ErrorWrapper(error: error, /* ... */))) } } case .onAppear: return .run { send in do {try await send(.onLoaded(load())) } catch { await send(.onError(ErrorWrapper(error: error, guidance: /* ... */))) } } case .errorSheetDismissed: state.scrumList.scrums = IdentifiedArray(uniqueElements: DailyScrum.sampleData) return .none case .binding: return .none case let .onError(errorWrapper): state.errorWrapper = errorWrapper return .none case let .onLoaded(scrums): state.scrumList.scrums = IdentifiedArray(uniqueElements: scrums) return .none } } } } struct ScrumdingerApp: App {
  49. @Reducer struct AppFeature { @ObservableState struct State: Equatable { /*

    ... */ var scrumList = ScrumsFeature.State(scrums: Shared(IdentifiedArray(uniqueElements: []))) var errorWrapper: ErrorWrapper? } enum Action: BindableAction { /* ... */ case scrumList(ScrumsFeature.Action) case onSave case onAppear case errorSheetDismissed case binding(BindingAction<State>) case onError(ErrorWrapper) case onLoaded([DailyScrum]) } var body: some ReducerOf<Self> { /* ... */ BindingReducer() Scope(state: \.scrumList, action: \.scrumList) { ScrumsFeature() } Reduce { state, action in switch action { case .scrumList: return .none case .onSave: return .run { [scrums = state.scrumList.scrums] send in do {try await save(scrums: scrums.elements) } catch { await send(.onError(ErrorWrapper(error: error, /* ... */))) } } case .onAppear: return .run { send in do {try await send(.onLoaded(load())) } catch { await send(.onError(ErrorWrapper(error: error, guidance: /* ... */))) } } case .errorSheetDismissed: state.scrumList.scrums = IdentifiedArray(uniqueElements: DailyScrum.sampleData) return .none case .binding: return .none /* ... */ case let .onError(errorWrapper): state.errorWrapper = errorWrapper return .none case let .onLoaded(scrums): state.scrumList.scrums = IdentifiedArray(uniqueElements: scrums) return .none } } } } struct ScrumdingerApp: App { @StateObject private var store = ScrumStore() @State private var errorWrapper: ErrorWrapper? var body: some Scene { WindowGroup { ScrumsView(scrums: $store.scrums) { Task { do { try await store.save(scrums: store.scrums) } catch {
  50. @Reducer struct AppFeature { @ObservableState struct State: Equatable { /*

    ... */ var scrumList = ScrumsFeature.State(scrums: Shared(IdentifiedArray(uniqueElements: []))) var errorWrapper: ErrorWrapper? } enum Action: BindableAction { /* ... */ case scrumList(ScrumsFeature.Action) case onSave case onAppear case errorSheetDismissed case binding(BindingAction<State>) case onError(ErrorWrapper) case onLoaded([DailyScrum]) } var body: some ReducerOf<Self> { /* ... */ BindingReducer() Scope(state: \.scrumList, action: \.scrumList) { ScrumsFeature() } Reduce { state, action in switch action { case .scrumList: return .none case .onSave: return .run { [scrums = state.scrumList.scrums] send in do { try await save(scrums: scrums.elements) } catch { await send(.onError(ErrorWrapper(error: error, /* ... */))) } } case .onAppear: return .run { send in do {try await send(.onLoaded(load())) } catch { await send(.onError(ErrorWrapper(error: error, guidance: /* ... */))) } } case .errorSheetDismissed: state.scrumList.scrums = IdentifiedArray(uniqueElements: DailyScrum.sampleData) return .none case .binding: return .none /* ... */ case let .onError(errorWrapper): state.errorWrapper = errorWrapper return .none case let .onLoaded(scrums): state.scrumList.scrums = IdentifiedArray(uniqueElements: scrums) return .none } } } } struct ScrumdingerApp: App { @StateObject private var store = ScrumStore() @State private var errorWrapper: ErrorWrapper? var body: some Scene { WindowGroup {
  51. @Reducer struct AppFeature { @ObservableState struct State: Equatable { /*

    ... */ var scrumList = ScrumsFeature.State(scrums: Shared(IdentifiedArray(uniqueElements: []))) var errorWrapper: ErrorWrapper? } enum Action: BindableAction { /* ... */ case scrumList(ScrumsFeature.Action) case onSave case onAppear case errorSheetDismissed case binding(BindingAction<State>) case onError(ErrorWrapper) case onLoaded([DailyScrum]) } var body: some ReducerOf<Self> { /* ... */ BindingReducer() Scope(state: \.scrumList, action: \.scrumList) { ScrumsFeature() } Reduce { state, action in switch action { case .scrumList: return .none case .onSave: return .run { [scrums = state.scrumList.scrums] send in do { try await save(scrums: scrums.elements) } catch { state.errorWrapper = ErrorWrapper(error: error, /* ... */) await send(.onError(ErrorWrapper(error: error, /* ... */))) } } case .onAppear: return .run { send in do {try await send(.onLoaded(load())) } catch { await send(.onError(ErrorWrapper(error: error, guidance: /* ... */))) } } case .errorSheetDismissed: state.scrumList.scrums = IdentifiedArray(uniqueElements: DailyScrum.sampleData) return .none case .binding: return .none /* ... */ case let .onError(errorWrapper): state.errorWrapper = errorWrapper return .none case let .onLoaded(scrums): state.scrumList.scrums = IdentifiedArray(uniqueElements: scrums) return .none } } } } struct ScrumdingerApp: App { @StateObject private var store = ScrumStore() @State private var errorWrapper: ErrorWrapper? var body: some Scene { WindowGroup {
  52. @Reducer struct AppFeature { @ObservableState struct State: Equatable { /*

    ... */ var scrumList = ScrumsFeature.State(scrums: Shared(IdentifiedArray(uniqueElements: []))) var errorWrapper: ErrorWrapper? } enum Action: BindableAction { /* ... */ case scrumList(ScrumsFeature.Action) case onSave case onAppear case errorSheetDismissed case binding(BindingAction<State>) case onError(ErrorWrapper) case onLoaded([DailyScrum]) } var body: some ReducerOf<Self> { /* ... */ BindingReducer() Scope(state: \.scrumList, action: \.scrumList) { ScrumsFeature() } Reduce { state, action in switch action { case .scrumList: return .none case .onSave: return .run { [scrums = state.scrumList.scrums] send in do { try await save(scrums: scrums.elements) } catch { state.errorWrapper = ErrorWrapper(error: error, /* ... */) await send(.onError(ErrorWrapper(error: error, /* ... */))) } } case .onAppear: return .run { send in do {try await send(.onLoaded(load())) } catch { await send(.onError(ErrorWrapper(error: error, guidance: /* ... */))) } } case .errorSheetDismissed: state.scrumList.scrums = IdentifiedArray(uniqueElements: DailyScrum.sampleData) return .none case .binding: return .none /* ... */ case let .onError(errorWrapper): state.errorWrapper = errorWrapper return .none case let .onLoaded(scrums): state.scrumList.scrums = IdentifiedArray(uniqueElements: scrums) return .none } } } } struct ScrumdingerApp: App { @StateObject private var store = ScrumStore() @State private var errorWrapper: ErrorWrapper? var body: some Scene {
  53. @Reducer struct AppFeature { @ObservableState struct State: Equatable { /*

    ... */ var scrumList = ScrumsFeature.State(scrums: Shared(IdentifiedArray(uniqueElements: []))) var errorWrapper: ErrorWrapper? } enum Action: BindableAction { /* ... */ case scrumList(ScrumsFeature.Action) case onSave case onAppear case errorSheetDismissed case binding(BindingAction<State>) case onError(ErrorWrapper) case onLoaded([DailyScrum]) } var body: some ReducerOf<Self> { /* ... */ BindingReducer() Scope(state: \.scrumList, action: \.scrumList) { ScrumsFeature() } Reduce { state, action in switch action { case .scrumList: return .none case .onSave: return .run { [scrums = state.scrumList.scrums] send in do { try await save(scrums: scrums.elements) } catch { await send(.onError(ErrorWrapper(error: error, /* ... */))) } } case .onAppear: return .run { send in do {try await send(.onLoaded(load())) } catch { await send(.onError(ErrorWrapper(error: error, guidance: /* ... */))) } } case .errorSheetDismissed: state.scrumList.scrums = IdentifiedArray(uniqueElements: DailyScrum.sampleData) return .none case .binding: return .none /* ... */ case let .onError(errorWrapper): state.errorWrapper = errorWrapper return .none case let .onLoaded(scrums): state.scrumList.scrums = IdentifiedArray(uniqueElements: scrums) return .none } } } } struct ScrumdingerApp: App { @StateObject private var store = ScrumStore() @State private var errorWrapper: ErrorWrapper? var body: some Scene {
  54. @Reducer struct AppFeature { @ObservableState struct State: Equatable { /*

    ... */ var scrumList = ScrumsFeature.State(scrums: Shared(IdentifiedArray(uniqueElements: []))) var errorWrapper: ErrorWrapper? } enum Action: BindableAction { /* ... */ case scrumList(ScrumsFeature.Action) case onSave case onAppear case errorSheetDismissed case binding(BindingAction<State>) case onError(ErrorWrapper) case onLoaded([DailyScrum]) } var body: some ReducerOf<Self> { /* ... */ BindingReducer() Scope(state: \.scrumList, action: \.scrumList) { ScrumsFeature() } Reduce { state, action in switch action { case .scrumList: return .none case .onSave: return .run { [scrums = state.scrumList.scrums] send in do { try await save(scrums: scrums.elements) } catch { await send(.onError(ErrorWrapper(error: error, /* ... */))) } } case .onAppear: return .run { send in do {try await send(.onLoaded(load())) } catch { await send(.onError(ErrorWrapper(error: error, guidance: /* ... */))) } } case .errorSheetDismissed: state.scrumList.scrums = IdentifiedArray(uniqueElements: DailyScrum.sampleData) return .none case .binding: return .none /* ... */ case let .onError(errorWrapper): state.errorWrapper = errorWrapper return .none case let .onLoaded(scrums): state.scrumList.scrums = IdentifiedArray(uniqueElements: scrums) return .none } } } } struct ScrumdingerApp: App { @StateObject private var store = ScrumStore()
  55. @Reducer struct AppFeature { @ObservableState struct State: Equatable { /*

    ... */ var scrumList = ScrumsFeature.State(scrums: Shared(IdentifiedArray(uniqueElements: []))) var errorWrapper: ErrorWrapper? } enum Action: BindableAction { /* ... */ case scrumList(ScrumsFeature.Action) case onSave case onAppear case errorSheetDismissed case binding(BindingAction<State>) case onError(ErrorWrapper) case onLoaded([DailyScrum]) } var body: some ReducerOf<Self> { /* ... */ BindingReducer() Scope(state: \.scrumList, action: \.scrumList) { ScrumsFeature() } Reduce { state, action in switch action { case .scrumList: return .none case .onSave: return .run { [scrums = state.scrumList.scrums] send in do {try await save(scrums: scrums.elements) } catch { await send(.onError(ErrorWrapper(error: error, /* ... */))) } } case .onAppear: return .run { send in do {try await send(.onLoaded(load())) } catch { await send(.onError(ErrorWrapper(error: error, guidance: /* ... */))) } } case .errorSheetDismissed: state.scrumList.scrums = IdentifiedArray(uniqueElements: DailyScrum.sampleData) return .none case .binding: return .none /* ... */ case let .onError(errorWrapper): state.errorWrapper = errorWrapper return .none case let .onLoaded(scrums): state.scrumList.scrums = IdentifiedArray(uniqueElements: scrums) return .none } } } } struct ScrumdingerApp: App { @StateObject private var store = ScrumStore() @State private var errorWrapper: ErrorWrapper? var body: some Scene { WindowGroup { ScrumsView(scrums: $store.scrums) { Task { do { try await store.save(scrums: store.scrums)
  56. @Reducer struct AppFeature { @ObservableState struct State: Equatable { /*

    ... */ var scrumList = ScrumsFeature.State(scrums: Shared(IdentifiedArray(uniqueElements: []))) var errorWrapper: ErrorWrapper? } enum Action: BindableAction { /* ... */ case scrumList(ScrumsFeature.Action) case onSave case onAppear case errorSheetDismissed case binding(BindingAction<State>) case onError(ErrorWrapper) case onLoaded([DailyScrum]) } var body: some ReducerOf<Self> { /* ... */ BindingReducer() Scope(state: \.scrumList, action: \.scrumList) { ScrumsFeature() } Reduce { state, action in switch action { case .scrumList: return .none case .onSave: return .run { [scrums = state.scrumList.scrums] send in do {try await save(scrums: scrums.elements) } catch { await send(.onError(ErrorWrapper(error: error, /* ... */))) } } case .onAppear: return .run { send in do { state.scrumList.scrums = IdentifiedArray(uniqueElements: try await load()) try await send(.onLoaded(load())) } catch { await send(.onError(ErrorWrapper(error: error, guidance: /* ... */))) } } case .errorSheetDismissed: state.scrumList.scrums = IdentifiedArray(uniqueElements: DailyScrum.sampleData) return .none case .binding: return .none /* ... */ case let .onError(errorWrapper): state.errorWrapper = errorWrapper return .none case let .onLoaded(scrums): state.scrumList.scrums = IdentifiedArray(uniqueElements: scrums) return .none } } } } struct ScrumdingerApp: App { @StateObject private var store = ScrumStore() @State private var errorWrapper: ErrorWrapper?
  57. @Reducer struct AppFeature { @ObservableState struct State: Equatable { /*

    ... */ var scrumList = ScrumsFeature.State(scrums: Shared(IdentifiedArray(uniqueElements: []))) var errorWrapper: ErrorWrapper? } enum Action: BindableAction { /* ... */ case scrumList(ScrumsFeature.Action) case onSave case onAppear case errorSheetDismissed case binding(BindingAction<State>) case onError(ErrorWrapper) case onLoaded([DailyScrum]) } var body: some ReducerOf<Self> { /* ... */ BindingReducer() Scope(state: \.scrumList, action: \.scrumList) { ScrumsFeature() } Reduce { state, action in switch action { case .scrumList: return .none case .onSave: return .run { [scrums = state.scrumList.scrums] send in do {try await save(scrums: scrums.elements) } catch { await send(.onError(ErrorWrapper(error: error, /* ... */))) } } case .onAppear: return .run { send in do { state.scrumList.scrums = IdentifiedArray(uniqueElements: try await load()) try await send(.onLoaded(load())) } catch { await send(.onError(ErrorWrapper(error: error, guidance: /* ... */))) } } case .errorSheetDismissed: state.scrumList.scrums = IdentifiedArray(uniqueElements: DailyScrum.sampleData) return .none case .binding: return .none /* ... */ case let .onError(errorWrapper): state.errorWrapper = errorWrapper return .none case let .onLoaded(scrums): state.scrumList.scrums = IdentifiedArray(uniqueElements: scrums) return .none } } } }
  58. @Reducer struct AppFeature { @ObservableState struct State: Equatable { /*

    ... */ var scrumList = ScrumsFeature.State(scrums: Shared(IdentifiedArray(uniqueElements: []))) var errorWrapper: ErrorWrapper? } enum Action: BindableAction { /* ... */ case scrumList(ScrumsFeature.Action) case onSave case onAppear case errorSheetDismissed case binding(BindingAction<State>) case onError(ErrorWrapper) case onLoaded([DailyScrum]) } var body: some ReducerOf<Self> { /* ... */ BindingReducer() Scope(state: \.scrumList, action: \.scrumList) { ScrumsFeature() } Reduce { state, action in switch action { case .scrumList: return .none case .onSave: return .run { [scrums = state.scrumList.scrums] send in do {try await save(scrums: scrums.elements) } catch { await send(.onError(ErrorWrapper(error: error, /* ... */))) } } case .onAppear: return .run { send in do {state.scrumList.scrums = IdentifiedArray(uniqueElements: try await load()) try await send(.onLoaded(load())) } catch { await send(.onError(ErrorWrapper(error: error, guidance: /* ... */))) } } case .errorSheetDismissed: state.scrumList.scrums = IdentifiedArray(uniqueElements: DailyScrum.sampleData) return .none case .binding: return .none /* ... */ case let .onError(errorWrapper): state.errorWrapper = errorWrapper return .none case let .onLoaded(scrums): state.scrumList.scrums = IdentifiedArray(uniqueElements: scrums) return .none } } } }
  59. } } struct ScrumdingerApp: App { @StateObject private var store

    = ScrumStore() @State private var errorWrapper: ErrorWrapper? var body: some Scene { WindowGroup { ScrumsView(scrums: $store.scrums) { Task { do { try await store.save(scrums: store.scrums) } catch { errorWrapper = ErrorWrapper(error: error, guidance: "Try again later.") } } store.send(.onSave) } .task { store.send(.onAppear) do { try await store.load() } catch { errorWrapper = ErrorWrapper( error: error, guidance: "Scrumdinger will load sample data and continue.” ) } } .sheet(item: $errorWrapper) { store.scrums = DailyScrum.sampleData } content: { wrapper in ErrorView(errorWrapper: wrapper) } } } }
  60. } } struct ScrumdingerApp: App { @Bindable var store =

    Store(initialState: AppFeature.State(), reducer: { AppFeature() }) @StateObject private var store = ScrumStore() @State private var errorWrapper: ErrorWrapper? var body: some Scene { WindowGroup { ScrumsView(scrums: $store.scrums) { Task { do { try await store.save(scrums: store.scrums) } catch { errorWrapper = ErrorWrapper(error: error, guidance: "Try again later.") } } store.send(.onSave) } .task { store.send(.onAppear) do { try await store.load() } catch { errorWrapper = ErrorWrapper( error: error, guidance: "Scrumdinger will load sample data and continue.” ) } } .sheet(item: $errorWrapper) { store.scrums = DailyScrum.sampleData } content: { wrapper in ErrorView(errorWrapper: wrapper) } } } }
  61. } } struct ScrumdingerApp: App { @Bindable var store =

    Store(initialState: AppFeature.State(), reducer: { AppFeature() }) @StateObject private var store = ScrumStore() @State private var errorWrapper: ErrorWrapper? var body: some Scene { WindowGroup { ScrumsView(store: store.scope(state: \.scrumList, action: \.scrumList)) { Task { do { try await store.save(scrums: store.scrums) } catch { errorWrapper = ErrorWrapper(error: error, guidance: "Try again later.") } } store.send(.onSave) } .task { store.send(.onAppear) do { try await store.load() } catch { errorWrapper = ErrorWrapper( error: error, guidance: "Scrumdinger will load sample data and continue.” ) } } .sheet(item: $store.errorWrapper) { store.scrums = DailyScrum.sampleData } content: { wrapper in ErrorView(errorWrapper: wrapper) } } } }
  62. } } struct ScrumdingerApp: App { @Bindable var store =

    Store(initialState: AppFeature.State(), reducer: { AppFeature() }) @StateObject private var store = ScrumStore() @State private var errorWrapper: ErrorWrapper? var body: some Scene { WindowGroup { ScrumsView(store: store.scope(state: \.scrumList, action: \.scrumList)) { Task { do {try await store.save(scrums: store.scrums) } catch { errorWrapper = ErrorWrapper(error: error, guidance: "Try again later.") } } store.send(.onSave) } .task { store.send(.onAppear) do { try await store.load() } catch { errorWrapper = ErrorWrapper( error: error, guidance: "Scrumdinger will load sample data and continue.” ) } } .sheet(item: $store.errorWrapper) { store.scrums = DailyScrum.sampleData } content: { wrapper in ErrorView(errorWrapper: wrapper) } } } }
  63. } } struct ScrumdingerApp: App { @Bindable var store =

    Store(initialState: AppFeature.State(), reducer: { AppFeature() }) @StateObject private var store = ScrumStore() @State private var errorWrapper: ErrorWrapper? var body: some Scene { WindowGroup { ScrumsView(store: store.scope(state: \.scrumList, action: \.scrumList)) { Task { do {try await store.save(scrums: store.scrums) } catch { errorWrapper = ErrorWrapper(error: error, guidance: "Try again later.") } } store.send(.onSave) } .task { store.send(.onAppear) do {try await store.load() } catch { errorWrapper = ErrorWrapper( error: error, guidance: "Scrumdinger will load sample data and continue.” ) } } .sheet(item: $store.errorWrapper) { store.scrums = DailyScrum.sampleData } content: { wrapper in ErrorView(errorWrapper: wrapper) } } } }
  64. } } struct ScrumdingerApp: App { @Bindable var store =

    Store(initialState: AppFeature.State(), reducer: { AppFeature() }) @StateObject private var store = ScrumStore() @State private var errorWrapper: ErrorWrapper? var body: some Scene { WindowGroup { ScrumsView(store: store.scope(state: \.scrumList, action: \.scrumList)) { Task { do {try await store.save(scrums: store.scrums) } catch { errorWrapper = ErrorWrapper(error: error, guidance: "Try again later.") } } store.send(.onSave) } .task { store.send(.onAppear) do {try await store.load() } catch { errorWrapper = ErrorWrapper( error: error, guidance: "Scrumdinger will load sample data and continue.” ) } } .sheet(item: $store.errorWrapper) { store.send(.errorSheetDismissed) store.scrums = DailyScrum.sampleData } content: { wrapper in ErrorView(errorWrapper: wrapper) } } } }
  65. } } struct NewScrumSheet: View { @State private var newScrum

    = DailyScrum.emptyScrum @Binding var scrums: [DailyScrum] // TODO: Use IdentifiedArray @Binding var isPresentingNewScrumView: Bool var body: some View { NavigationStack { DetailEditView(scrum: $newScrum) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Dismiss") { isPresentingNewScrumView = false } } ToolbarItem(placement: .confirmationAction) { Button("Add") { scrums.append(newScrum) isPresentingNewScrumView = false } } } } } }
  66. @Reducer struct NewScrumFeature { @ObservableState struct State: Equatable { }

    enum Action { case cancelAddButtonTapped case confirmAddButtonTapped case binding(BindingAction<State>) } @Dependency(\.dismiss) var dismiss var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .cancelAddButtonTapped: return .none case .confirmAddButtonTapped: return .none case .binding: return .none } } } } struct NewScrumSheet: View { @State private var newScrum = DailyScrum.emptyScrum @Binding var scrums: [DailyScrum] // TODO: Use IdentifiedArray @Binding var isPresentingNewScrumView: Bool var body: some View { NavigationStack { DetailEditView(scrum: $newScrum) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Dismiss") { isPresentingNewScrumView = false } } ToolbarItem(placement: .confirmationAction) { Button("Add") { scrums.append(newScrum)
  67. @Reducer struct NewScrumFeature { @ObservableState struct State: Equatable { var

    newScrum = DailyScrum.emptyScrum @Shared var scrums: IdentifiedArrayOf<DailyScrum> @Shared var isPresentingNewScrumView: Bool } enum Action { case cancelAddButtonTapped case confirmAddButtonTapped case binding(BindingAction<State>) } @Dependency(\.dismiss) var dismiss var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .cancelAddButtonTapped: return .none case .confirmAddButtonTapped: return .none case .binding: return .none } } } } struct NewScrumSheet: View { var body: some View { NavigationStack { DetailEditView(scrum: $newScrum) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Dismiss") { isPresentingNewScrumView = false } } ToolbarItem(placement: .confirmationAction) { Button("Add") { scrums.append(newScrum)
  68. @Reducer struct NewScrumFeature { @ObservableState struct State: Equatable { var

    newScrum = DailyScrum.emptyScrum @Shared var scrums: IdentifiedArrayOf<DailyScrum> @Shared var isPresentingNewScrumView: Bool } enum Action: BindableAction { case cancelAddButtonTapped case confirmAddButtonTapped case binding(BindingAction<State>) } @Dependency(\.dismiss) var dismiss var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .cancelAddButtonTapped: return .none case .confirmAddButtonTapped: return .none case .binding: return .none } } } } struct NewScrumSheet: View { var body: some View { NavigationStack { DetailEditView(scrum: $newScrum) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Dismiss") { isPresentingNewScrumView = false } } ToolbarItem(placement: .confirmationAction) {
  69. @Reducer struct NewScrumFeature { @ObservableState struct State: Equatable { var

    newScrum = DailyScrum.emptyScrum @Shared var scrums: IdentifiedArrayOf<DailyScrum> @Shared var isPresentingNewScrumView: Bool } enum Action: BindableAction { case cancelAddButtonTapped case confirmAddButtonTapped case binding(BindingAction<State>) } @Dependency(\.dismiss) var dismiss var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .cancelAddButtonTapped: return .none case .confirmAddButtonTapped: return .none case .binding: return .none } } } } struct NewScrumSheet: View { var body: some View { NavigationStack {
  70. @Reducer struct NewScrumFeature { @ObservableState struct State: Equatable { var

    newScrum = DailyScrum.emptyScrum @Shared var scrums: IdentifiedArrayOf<DailyScrum> @Shared var isPresentingNewScrumView: Bool } enum Action: BindableAction { case cancelAddButtonTapped case confirmAddButtonTapped case binding(BindingAction<State>) } @Dependency(\.dismiss) var dismiss var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .cancelAddButtonTapped: state.isPresentingNewScrumView = false return .none case .confirmAddButtonTapped: state.scrums.append(state.newScrum) state.isPresentingNewScrumView = false return .none case .binding: return .none } } } } struct NewScrumSheet: View {
  71. @Reducer struct NewScrumFeature { @ObservableState struct State: Equatable { var

    newScrum = DailyScrum.emptyScrum @Shared var scrums: IdentifiedArrayOf<DailyScrum> @Shared var isPresentingNewScrumView: Bool } enum Action: BindableAction { case cancelAddButtonTapped case confirmAddButtonTapped case binding(BindingAction<State>) } @Dependency(\.dismiss) var dismiss var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .cancelAddButtonTapped: state.isPresentingNewScrumView = false return .none case .confirmAddButtonTapped: state.scrums.append(state.newScrum) state.isPresentingNewScrumView = false return .none case .binding: return .none } } } } struct NewScrumSheet: View {
  72. @Reducer struct NewScrumFeature { @ObservableState struct State: Equatable { var

    newScrum = DailyScrum.emptyScrum @Shared var scrums: IdentifiedArrayOf<DailyScrum> @Shared var isPresentingNewScrumView: Bool } enum Action: BindableAction { case cancelAddButtonTapped case confirmAddButtonTapped case binding(BindingAction<State>) } @Dependency(\.dismiss) var dismiss var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .cancelAddButtonTapped: state.isPresentingNewScrumView = false return .run { _ in await self.dismiss() } case .confirmAddButtonTapped: state.scrums.append(state.newScrum) state.isPresentingNewScrumView = false return .run { _ in await self.dismiss() } case .binding: return .none } } } } struct NewScrumSheet: View {
  73. @Reducer struct NewScrumFeature { @ObservableState struct State: Equatable { var

    newScrum = DailyScrum.emptyScrum @Shared var scrums: IdentifiedArrayOf<DailyScrum> } enum Action: BindableAction { case cancelAddButtonTapped case confirmAddButtonTapped case binding(BindingAction<State>) } @Dependency(\.dismiss) var dismiss var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .cancelAddButtonTapped: return .run { _ in await self.dismiss() } case .confirmAddButtonTapped: state.scrums.append(state.newScrum) return .run { _ in await self.dismiss() } case .binding: return .none } } } } struct NewScrumSheet: View { var body: some View { NavigationStack {
  74. } } struct NewScrumSheet: View { var body: some View

    { NavigationStack { DetailEditView(scrum: $newScrum) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Dismiss") { } } ToolbarItem(placement: .confirmationAction) { Button("Add") { } } } } } }
  75. } } struct NewScrumSheet: View { @Bindable var store: StoreOf<NewScrumFeature>

    var body: some View { NavigationStack { DetailEditView(scrum: $newScrum) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Dismiss") { } } ToolbarItem(placement: .confirmationAction) { Button("Add") { } } } } } }
  76. } } struct NewScrumSheet: View { @Bindable var store: StoreOf<NewScrumFeature>

    var body: some View { NavigationStack { DetailEditView(scrum: $store.newScrum) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Dismiss") { } } ToolbarItem(placement: .confirmationAction) { Button("Add") { } } } } } }
  77. } } struct NewScrumSheet: View { @Bindable var store: StoreOf<NewScrumFeature>

    var body: some View { NavigationStack { DetailEditView(scrum: $store.newScrum) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Dismiss") { store.send(.cancelAddButtonTapped) } } ToolbarItem(placement: .confirmationAction) { Button("Add") { store.send(.confirmAddButtonTapped) } } } } } }
  78. struct ScrumsView: View { @Bindable var store: StoreOf<ScrumsFeature> @Environment(\.scenePhase) private

    var scenePhase let saveAction: () -> Void var body: some View { NavigationStack(path: $store.scope(state: \.path, action: \.path)) { /* ... */ } .sheet(isPresented: $stroe.isPresentingNewScrumView) { // TODO: Use TCA’s IdentifiedArray in NewScrumSheet NewScrumSheet(scrums: $store.scrums, isPresentingNewScrumView: $store.isPresentingNewScrumView) } /* ... */ } } #Preview { ScrumsView( store: Store(initialState: ScrumsFeature.State(scrums: DailyScrum.sampleData)) { ScrumsFeature() }, saveAction: {} ) }
  79. @Reducer struct ScrumsFeature { @ObservableState struct State: Equatable { @Shared

    var scrums: IdentifiedArrayOf<DailyScrum> var isPresentingNewScrumView = false @Presents var newScrum: NewScrumFeature.State? var path = StackState<DetailFeature.State>() } enum Action: BindableAction { /* ... */ case newScrum(PresentationAction<NewScrumFeature.Action>) } var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .newScrumButtonTapped: state.isPresentingNewScrumView = true return .none /* ... */ case .newScrum: return .none } } .forEach(\.path, action: \.path) { DetailFeature() } .ifLet(\.$newScrum, action: \.newScrum) { NewScrumFeature() } } } struct ScrumsView: View { @Bindable var store: StoreOf<ScrumsFeature> @Environment(\.scenePhase) private var scenePhase let saveAction: () -> Void
  80. @Reducer struct ScrumsFeature { @ObservableState struct State: Equatable { @Shared

    var scrums: IdentifiedArrayOf<DailyScrum> @Presents var newScrum: NewScrumFeature.State? var path = StackState<DetailFeature.State>() } enum Action: BindableAction { /* ... */ case newScrum(PresentationAction<NewScrumFeature.Action>) } var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .newScrumButtonTapped: state.isPresentingNewScrumView = true return .none /* ... */ case .newScrum: return .none } } .forEach(\.path, action: \.path) { DetailFeature() } .ifLet(\.$newScrum, action: \.newScrum) { NewScrumFeature() } } } struct ScrumsView: View { @Bindable var store: StoreOf<ScrumsFeature> @Environment(\.scenePhase) private var scenePhase let saveAction: () -> Void
  81. @Reducer struct ScrumsFeature { @ObservableState struct State: Equatable { @Shared

    var scrums: IdentifiedArrayOf<DailyScrum> @Presents var newScrum: NewScrumFeature.State? var path = StackState<DetailFeature.State>() } enum Action: BindableAction { /* ... */ case newScrum(PresentationAction<NewScrumFeature.Action>) } var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .newScrumButtonTapped: state.newScrum = NewScrumFeature.State(scrums: state.$scrums) return .none /* ... */ case .newScrum: return .none } } .forEach(\.path, action: \.path) { DetailFeature() } .ifLet(\.$newScrum, action: \.newScrum) { NewScrumFeature() } } } struct ScrumsView: View { @Bindable var store: StoreOf<ScrumsFeature> @Environment(\.scenePhase) private var scenePhase let saveAction: () -> Void
  82. @Reducer struct ScrumsFeature { @ObservableState struct State: Equatable { @Shared

    var scrums: IdentifiedArrayOf<DailyScrum> @Presents var newScrum: NewScrumFeature.State? var path = StackState<DetailFeature.State>() } enum Action: BindableAction { /* ... */ case newScrum(PresentationAction<NewScrumFeature.Action>) } var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .newScrumButtonTapped: state.newScrum = NewScrumFeature.State(scrums: state.$scrums) return .none /* ... */ case .newScrum: return .none } } .forEach(\.path, action: \.path) { DetailFeature() } .ifLet(\.$newScrum, action: \.newScrum) { NewScrumFeature() } } } struct ScrumsView: View { @Bindable var store: StoreOf<ScrumsFeature> @Environment(\.scenePhase) private var scenePhase let saveAction: () -> Void
  83. @Reducer struct ScrumsFeature { @ObservableState struct State: Equatable { @Shared

    var scrums: IdentifiedArrayOf<DailyScrum> @Presents var newScrum: NewScrumFeature.State? var path = StackState<DetailFeature.State>() } enum Action: BindableAction { /* ... */ case newScrum(PresentationAction<NewScrumFeature.Action>) } var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .newScrumButtonTapped: state.newScrum = NewScrumFeature.State(scrums: state.$scrums) return .none /* ... */ case .newScrum: return .none } } .forEach(\.path, action: \.path) { DetailFeature() } .ifLet(\.$newScrum, action: \.newScrum) { NewScrumFeature() } } } struct ScrumsView: View { @Bindable var store: StoreOf<ScrumsFeature> @Environment(\.scenePhase) private var scenePhase
  84. @Reducer struct ScrumsFeature { @ObservableState struct State: Equatable { @Shared

    var scrums: IdentifiedArrayOf<DailyScrum> @Presents var newScrum: NewScrumFeature.State? var path = StackState<DetailFeature.State>() } enum Action: BindableAction { /* ... */ case newScrum(PresentationAction<NewScrumFeature.Action>) } var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .newScrumButtonTapped: state.newScrum = NewScrumFeature.State(scrums: state.$scrums) return .none /* ... */ case .newScrum: return .none } } .forEach(\.path, action: \.path) { DetailFeature() } .ifLet(\.$newScrum, action: \.newScrum) { NewScrumFeature() } } }
  85. } } struct ScrumsView: View { @Bindable var store: StoreOf<ScrumsFeature>

    @Environment(\.scenePhase) private var scenePhase let saveAction: () -> Void var body: some View { NavigationStack(path: $store.scope(state: \.path, action: \.path)) { /* ... */ } .sheet(isPresented: $stroe.isPresentingNewScrumView) { // TODO: Use TCA’s IdentifiedArray in NewScrumSheet NewScrumSheet(scrums: $store.scrums, isPresentingNewScrumView: $store.isPresentingNewScrumView) } /* ... */ } } #Preview { ScrumsView( store: Store(initialState: ScrumsFeature.State(scrums: DailyScrum.sampleData)) { ScrumsFeature() }, saveAction: {} ) }
  86. } } struct ScrumsView: View { @Bindable var store: StoreOf<ScrumsFeature>

    @Environment(\.scenePhase) private var scenePhase let saveAction: () -> Void var body: some View { NavigationStack(path: $store.scope(state: \.path, action: \.path)) { /* ... */ } .sheet(item: $store.scope(state: \.newScrum, action: \.newScrum)) { newScrumStore in // TODO: Use TCA’s IdentifiedArray in NewScrumSheet NewScrumSheet(scrums: $store.scrums, isPresentingNewScrumView: $store.isPresentingNewScrumView) } /* ... */ } } #Preview { ScrumsView( store: Store(initialState: ScrumsFeature.State(scrums: DailyScrum.sampleData)) { ScrumsFeature() }, saveAction: {} ) }
  87. } } struct ScrumsView: View { @Bindable var store: StoreOf<ScrumsFeature>

    @Environment(\.scenePhase) private var scenePhase let saveAction: () -> Void var body: some View { NavigationStack(path: $store.scope(state: \.path, action: \.path)) { /* ... */ } .sheet(item: $store.scope(state: \.newScrum, action: \.newScrum)) { newScrumStore in NewScrumSheet(store: newScrumStore) } /* ... */ } } #Preview { ScrumsView( store: Store(initialState: ScrumsFeature.State(scrums: DailyScrum.sampleData)) { ScrumsFeature() }, saveAction: {} ) }
  88. } } struct DetailEditView: View { @Binding var scrum: DailyScrum

    @State private var newAttendeeName = "" var body: some View { Form { Section(header: Text("Meeting Info")) { TextField("Title", text: $scrum.title) HStack { Slider(value: $scrum.lengthInMinutesAsDouble, in: 5...30, step: 1) { Text("Length") } Spacer() Text("\(scrum.lengthInMinutes) minutes") } ThemePicker(selection: $scrum.theme) } Section(header: Text("Attendees")) { ForEach(scrum.attendees) { attendee in Text(attendee.name) } .onDelete { indices in scrum.attendees.remove(atOffsets: indices) } HStack { TextField("New Attendee", text: $newAttendeeName) Button(action: { withAnimation { let attendee = DailyScrum.Attendee(name: newAttendeeName) scrum.attendees.append(attendee) newAttendeeName = "" } }) {
  89. } } struct DetailEditView: View { @Binding var scrum: DailyScrum

    @State private var newAttendeeName = "" var body: some View { Form { Section(header: Text("Meeting Info")) { /* ... */ TextField("Title", text: $scrum.title) HStack { Slider(value: $scrum.lengthInMinutesAsDouble, in: 5...30, step: 1) { Text("Length") } Spacer() Text("\(scrum.lengthInMinutes) minutes") } ThemePicker(selection: $scrum.theme) } Section(header: Text("Attendees")) { ForEach(scrum.attendees) { attendee in Text(attendee.name) } .onDelete { indices in scrum.attendees.remove(atOffsets: indices) } HStack { TextField("New Attendee", text: $newAttendeeName) Button(action: { withAnimation { let attendee = DailyScrum.Attendee(name: newAttendeeName) scrum.attendees.append(attendee) newAttendeeName = "" } }) { Image(systemName: "plus.circle.fill") } .disabled(newAttendeeName.isEmpty) } } } } }
  90. @Reducer struct DetailEditFeature { @ObservableState struct State: Equatable { }

    enum Action { case onDeleteAttendees(IndexSet) case addAttendeeButtonTapped case binding(BindingAction<State>) } var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case let .onDeleteAttendees(indices): return .none case .addAttendeeButtonTapped: return .none case .binding: return .none } } } } struct DetailEditView: View { @Binding var scrum: DailyScrum @State private var newAttendeeName = "" var body: some View { Form { Section(header: Text("Meeting Info")) { /* ... */ TextField("Title", text: $scrum.title) HStack { Slider(value: $scrum.lengthInMinutesAsDouble, in: 5...30, step: 1) { Text("Length") } Spacer() Text("\(scrum.lengthInMinutes) minutes") } ThemePicker(selection: $scrum.theme) } Section(header: Text("Attendees")) { ForEach(scrum.attendees) { attendee in Text(attendee.name) } .onDelete { indices in scrum.attendees.remove(atOffsets: indices) } HStack {
  91. @Reducer struct DetailEditFeature { @ObservableState struct State: Equatable { var

    scrum: DailyScrum = .emptyScrum var newAttendeeName = "" } enum Action { case onDeleteAttendees(IndexSet) case addAttendeeButtonTapped case binding(BindingAction<State>) } var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case let .onDeleteAttendees(indices): return .none case .addAttendeeButtonTapped: return .none case .binding: return .none } } } } struct DetailEditView: View { var body: some View { Form { Section(header: Text("Meeting Info")) { /* ... */ TextField("Title", text: $scrum.title) HStack { Slider(value: $scrum.lengthInMinutesAsDouble, in: 5...30, step: 1) { Text("Length") } Spacer() Text("\(scrum.lengthInMinutes) minutes") } ThemePicker(selection: $scrum.theme) } Section(header: Text("Attendees")) { ForEach(scrum.attendees) { attendee in Text(attendee.name) } .onDelete { indices in scrum.attendees.remove(atOffsets: indices) } HStack {
  92. @Reducer struct DetailEditFeature { @ObservableState struct State: Equatable { var

    scrum: DailyScrum = .emptyScrum var newAttendeeName = "" } enum Action: BindableAction { case onDeleteAttendees(IndexSet) case addAttendeeButtonTapped case binding(BindingAction<State>) } var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case let .onDeleteAttendees(indices): return .none case .addAttendeeButtonTapped: return .none case .binding: return .none } } } } struct DetailEditView: View { var body: some View { Form { Section(header: Text("Meeting Info")) { /* ... */ TextField("Title", text: $scrum.title) HStack { Slider(value: $scrum.lengthInMinutesAsDouble, in: 5...30, step: 1) { Text("Length") } Spacer() Text("\(scrum.lengthInMinutes) minutes") } ThemePicker(selection: $scrum.theme) } Section(header: Text("Attendees")) { ForEach(scrum.attendees) { attendee in Text(attendee.name) } .onDelete { indices in scrum.attendees.remove(atOffsets: indices)
  93. @Reducer struct DetailEditFeature { @ObservableState struct State: Equatable { var

    scrum: DailyScrum = .emptyScrum var newAttendeeName = "" } enum Action: BindableAction { case onDeleteAttendees(IndexSet) case addAttendeeButtonTapped case binding(BindingAction<State>) } var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case let .onDeleteAttendees(indices): return .none case .addAttendeeButtonTapped: return .none case .binding: return .none } } } } struct DetailEditView: View { var body: some View { Form { Section(header: Text("Meeting Info")) { /* ... */ TextField("Title", text: $scrum.title) HStack { Slider(value: $scrum.lengthInMinutesAsDouble, in: 5...30, step: 1) { Text("Length") } Spacer() Text("\(scrum.lengthInMinutes) minutes") } ThemePicker(selection: $scrum.theme)
  94. @Reducer struct DetailEditFeature { @ObservableState struct State: Equatable { var

    scrum: DailyScrum = .emptyScrum var newAttendeeName = "" } enum Action: BindableAction { case onDeleteAttendees(IndexSet) case addAttendeeButtonTapped case binding(BindingAction<State>) } var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case let .onDeleteAttendees(indices): state.scrum.attendees.remove(atOffsets: indices) return .none case .addAttendeeButtonTapped: let attendee = DailyScrum.Attendee(name: state.newAttendeeName) state.scrum.attendees.append(attendee) state.newAttendeeName = "" return .none case .binding: return .none } } } } struct DetailEditView: View {
  95. } struct DetailEditView: View { var body: some View {

    Form { Section(header: Text("Meeting Info")) { /* ... */ TextField("Title", text: $scrum.title) HStack { Slider(value: $scrum.lengthInMinutesAsDouble, in: 5...30, step: 1) { Text("Length") } Spacer() Text("\(scrum.lengthInMinutes) minutes") } ThemePicker(selection: $scrum.theme) } Section(header: Text("Attendees")) { ForEach(scrum.attendees) { attendee in Text(attendee.name) } .onDelete { indices in } HStack { TextField("New Attendee", text: $newAttendeeName) Button(action: { withAnimation { } }) { Image(systemName: "plus.circle.fill") } .disabled(newAttendeeName.isEmpty) } } } } }
  96. } struct DetailEditView: View { @Bindable var store: StoreOf<DetailEditFeature> var

    body: some View { Form { Section(header: Text("Meeting Info")) { /* ... */ TextField("Title", text: $scrum.title) HStack { Slider(value: $scrum.lengthInMinutesAsDouble, in: 5...30, step: 1) { Text("Length") } Spacer() Text("\(scrum.lengthInMinutes) minutes") } ThemePicker(selection: $scrum.theme) } Section(header: Text("Attendees")) { ForEach(scrum.attendees) { attendee in Text(attendee.name) } .onDelete { indices in } HStack { TextField("New Attendee", text: $newAttendeeName) Button(action: { withAnimation { } }) { Image(systemName: "plus.circle.fill") } .disabled(newAttendeeName.isEmpty) } } } } }
  97. } struct DetailEditView: View { @Bindable var store: StoreOf<DetailEditFeature> var

    body: some View { Form { Section(header: Text("Meeting Info")) { /* ... */ TextField("Title", text: $scrum.title) HStack { Slider(value: $scrum.lengthInMinutesAsDouble, in: 5...30, step: 1) { Text("Length") } Spacer() Text("\(scrum.lengthInMinutes) minutes") } ThemePicker(selection: $scrum.theme) } Section(header: Text("Attendees")) { ForEach(store.scrum.attendees) { attendee in Text(attendee.name) } .onDelete { indices in } HStack { TextField("New Attendee", text: $store.newAttendeeName) Button(action: { withAnimation { } }) { Image(systemName: "plus.circle.fill") } .disabled(store.newAttendeeName.isEmpty) } } } } }
  98. } struct DetailEditView: View { @Bindable var store: StoreOf<DetailEditFeature> var

    body: some View { Form { Section(header: Text("Meeting Info")) { /* ... */ TextField("Title", text: $scrum.title) HStack { Slider(value: $scrum.lengthInMinutesAsDouble, in: 5...30, step: 1) { Text("Length") } Spacer() Text("\(scrum.lengthInMinutes) minutes") } ThemePicker(selection: $scrum.theme) } Section(header: Text("Attendees")) { ForEach(store.scrum.attendees) { attendee in Text(attendee.name) } .onDelete { indices in store.send(.onDeleteAttendees(indices)) } HStack { TextField("New Attendee", text: $store.newAttendeeName) Button(action: { withAnimation { store.send(.addAttendeeButtonTapped) } }) { Image(systemName: "plus.circle.fill") } .disabled(store.newAttendeeName.isEmpty) } } } } }
  99. } struct DetailEditView: View { @Bindable var store: StoreOf<DetailEditFeature> var

    body: some View { Form { Section(header: Text("Meeting Info")) { /* ... */ TextField("Title", text: $scrum.title) HStack { Slider(value: $scrum.lengthInMinutesAsDouble, in: 5...30, step: 1) { Text("Length") } Spacer() Text("\(scrum.lengthInMinutes) minutes") } ThemePicker(selection: $scrum.theme) } Section(header: Text("Attendees")) { ForEach(store.scrum.attendees) { attendee in Text(attendee.name) } .onDelete { indices in store.send(.onDeleteAttendees(indices)) } HStack { TextField("New Attendee", text: $store.newAttendeeName) Button(action: { store.send(.addAttendeeButtonTapped, animation: .default) }) { Image(systemName: "plus.circle.fill") } .disabled(store.newAttendeeName.isEmpty) } } } } }
  100. @Reducer struct NewScrumFeature { @ObservableState struct State: Equatable { var

    newScrum = DailyScrum.emptyScrum @Shared var scrums: IdentifiedArrayOf<DailyScrum> } enum Action: BindableAction { /* ... */ case editScrum(DetailEditFeature.Action) } @Dependency(\.dismiss) var dismiss var body: some ReducerOf<Self> { BindingReducer() Scope(state: \.editScrum, action: \.editScrum) { DetailEditFeature() } Reduce { state, action in switch action { case .cancelAddButtonTapped: return .run { _ in await self.dismiss() } case .confirmAddButtonTapped: state.scrums.append(state.newScrum) return .run { _ in await self.dismiss() } case .binding: return .none case .editScrum: return .none } } } } struct NewScrumSheet: View { @Bindable var store: StoreOf<NewScrumFeature> var body: some View { NavigationStack { DetailEditView(scrum: $store.newScrum) .toolbar {
  101. @Reducer struct NewScrumFeature { @ObservableState struct State: Equatable { var

    editScrum = DetailEditFeature.State(scrum: DailyScrum.emptyScrum) @Shared var scrums: IdentifiedArrayOf<DailyScrum> } enum Action: BindableAction { /* ... */ case editScrum(DetailEditFeature.Action) } @Dependency(\.dismiss) var dismiss var body: some ReducerOf<Self> { BindingReducer() Scope(state: \.editScrum, action: \.editScrum) { DetailEditFeature() } Reduce { state, action in switch action { case .cancelAddButtonTapped: return .run { _ in await self.dismiss() } case .confirmAddButtonTapped: state.scrums.append(state.newScrum) return .run { _ in await self.dismiss() } case .binding: return .none case .editScrum: return .none } } } } struct NewScrumSheet: View { @Bindable var store: StoreOf<NewScrumFeature> var body: some View { NavigationStack { DetailEditView(scrum: $store.newScrum) .toolbar {
  102. @Reducer struct NewScrumFeature { @ObservableState struct State: Equatable { var

    editScrum = DetailEditFeature.State(scrum: DailyScrum.emptyScrum) @Shared var scrums: IdentifiedArrayOf<DailyScrum> } enum Action: BindableAction { /* ... */ case editScrum(DetailEditFeature.Action) } @Dependency(\.dismiss) var dismiss var body: some ReducerOf<Self> { BindingReducer() Scope(state: \.editScrum, action: \.editScrum) { DetailEditFeature() } Reduce { state, action in switch action { case .cancelAddButtonTapped: return .run { _ in await self.dismiss() } case .confirmAddButtonTapped: state.scrums.append(state.editScrum.newScrum) return .run { _ in await self.dismiss() } case .binding: return .none case .editScrum: return .none } } } } struct NewScrumSheet: View { @Bindable var store: StoreOf<NewScrumFeature> var body: some View { NavigationStack { DetailEditView(scrum: $store.newScrum) .toolbar {
  103. @Reducer struct NewScrumFeature { @ObservableState struct State: Equatable { var

    editScrum = DetailEditFeature.State(scrum: DailyScrum.emptyScrum) @Shared var scrums: IdentifiedArrayOf<DailyScrum> } enum Action: BindableAction { /* ... */ case editScrum(DetailEditFeature.Action) } @Dependency(\.dismiss) var dismiss var body: some ReducerOf<Self> { BindingReducer() Scope(state: \.editScrum, action: \.editScrum) { DetailEditFeature() } Reduce { state, action in switch action { case .cancelAddButtonTapped: return .run { _ in await self.dismiss() } case .confirmAddButtonTapped: state.scrums.append(state.editScrum.newScrum) return .run { _ in await self.dismiss() } case .binding: return .none case .editScrum: return .none } } } } struct NewScrumSheet: View { @Bindable var store: StoreOf<NewScrumFeature> var body: some View {
  104. @Reducer struct NewScrumFeature { @ObservableState struct State: Equatable { var

    editScrum = DetailEditFeature.State(scrum: DailyScrum.emptyScrum) @Shared var scrums: IdentifiedArrayOf<DailyScrum> } enum Action: BindableAction { /* ... */ case editScrum(DetailEditFeature.Action) } @Dependency(\.dismiss) var dismiss var body: some ReducerOf<Self> { BindingReducer() Scope(state: \.editScrum, action: \.editScrum) { DetailEditFeature() } Reduce { state, action in switch action { case .cancelAddButtonTapped: return .run { _ in await self.dismiss() } case .confirmAddButtonTapped: state.scrums.append(state.editScrum.newScrum) return .run { _ in await self.dismiss() } case .binding: return .none case .editScrum: return .none } } } } struct NewScrumSheet: View {
  105. } struct NewScrumSheet: View { @Bindable var store: StoreOf<NewScrumFeature> var

    body: some View { NavigationStack { DetailEditView(scrum: $store.newScrum) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Dismiss") { store.send(.cancelAddButtonTapped) } } ToolbarItem(placement: .confirmationAction) { Button("Add") { store.send(.confirmAddButtonTapped) } } } } } }
  106. } struct NewScrumSheet: View { @Bindable var store: StoreOf<NewScrumFeature> var

    body: some View { NavigationStack { DetailEditView(store: store.scope(state: \.editScrum, action: \.editScrum)) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Dismiss") { store.send(.cancelAddButtonTapped) } } ToolbarItem(placement: .confirmationAction) { Button("Add") { store.send(.confirmAddButtonTapped) } } } } } }
  107. @Reducer struct DetailFeature { @Observable State struct State: Equatable {

    @Shared var scrum: DailyScrum var editingScrum = DailyScrum.emptyScrum var isPresentingEditView = false } enum Action: BindableAction { /* ... */ case editScrum(PresentationAction<DetailEditFeature.Action>) } var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .editButtonTapped: state.isPresentingEditView = true state.editingScrum = state.scrum return .none case .cancelEditButtonTapped: state.isPresentingEditView = false return .none case .confirmEditButtonTapped: state.isPresentingEditView = false state.scrum = state.editingScrum return .none /* ... */ case .editScrum: return .none } } .ifLet(\.$editScrum, action: \.editScrum) { DetailEditFeature() } } } struct DetailView: View {
  108. @Reducer struct DetailFeature { @Observable State struct State: Equatable {

    @Shared var scrum: DailyScrum @Presents var editScrum: DetailEditFeature.State? } enum Action: BindableAction { /* ... */ case editScrum(PresentationAction<DetailEditFeature.Action>) } var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .editButtonTapped: state.isPresentingEditView = true state.editingScrum = state.scrum return .none case .cancelEditButtonTapped: state.isPresentingEditView = false return .none case .confirmEditButtonTapped: state.isPresentingEditView = false state.scrum = state.editingScrum return .none /* ... */ case .editScrum: return .none } } .ifLet(\.$editScrum, action: \.editScrum) { DetailEditFeature() } } } struct DetailView: View { @Bindable var store: StoreOf<DetailFeature>
  109. @Reducer struct DetailFeature { @Observable State struct State: Equatable {

    @Shared var scrum: DailyScrum @Presents var editScrum: DetailEditFeature.State? } enum Action: BindableAction { /* ... */ case editScrum(PresentationAction<DetailEditFeature.Action>) } var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .editButtonTapped: state.editScrum = DetailEditFeature.State(scrum: state.scrum) return .none case .cancelEditButtonTapped: state.isPresentingEditView = false return .none case .confirmEditButtonTapped: state.isPresentingEditView = false state.scrum = state.editingScrum return .none /* ... */ case .editScrum: return .none } } .ifLet(\.$editScrum, action: \.editScrum) { DetailEditFeature() } } } struct DetailView: View { @Bindable var store: StoreOf<DetailFeature>
  110. @Reducer struct DetailFeature { @Observable State struct State: Equatable {

    @Shared var scrum: DailyScrum @Presents var editScrum: DetailEditFeature.State? } enum Action: BindableAction { /* ... */ case editScrum(PresentationAction<DetailEditFeature.Action>) } var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .editButtonTapped: state.editScrum = DetailEditFeature.State(scrum: state.scrum) return .none case .cancelEditButtonTapped: state.editScrum = nil return .none case .confirmEditButtonTapped: state.isPresentingEditView = false state.scrum = state.editingScrum if let scrum = state.editScrum?.scrum { state.scrum = scrum } state.editScrum = nil return .none /* ... */ case .editScrum: return .none } } .ifLet(\.$editScrum, action: \.editScrum) { DetailEditFeature() } } } struct DetailView: View { @Bindable var store: StoreOf<DetailFeature>
  111. @Reducer struct DetailFeature { @Observable State struct State: Equatable {

    @Shared var scrum: DailyScrum @Presents var editScrum: DetailEditFeature.State? } enum Action: BindableAction { /* ... */ case editScrum(PresentationAction<DetailEditFeature.Action>) } var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .editButtonTapped: state.editScrum = DetailEditFeature.State(scrum: state.scrum) return .none case .cancelEditButtonTapped: state.editScrum = nil return .none case .confirmEditButtonTapped: state.isPresentingEditView = false state.scrum = state.editingScrum if let scrum = state.editScrum?.scrum { state.scrum = scrum } state.editScrum = nil return .none /* ... */ case .editScrum: return .none } } .ifLet(\.$editScrum, action: \.editScrum) { DetailEditFeature() } } } struct DetailView: View { @Bindable var store: StoreOf<DetailFeature>
  112. @Reducer struct DetailFeature { @Observable State struct State: Equatable {

    @Shared var scrum: DailyScrum @Presents var editScrum: DetailEditFeature.State? } enum Action: BindableAction { /* ... */ case editScrum(PresentationAction<DetailEditFeature.Action>) } var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .editButtonTapped: state.editScrum = DetailEditFeature.State(scrum: state.scrum) return .none case .cancelEditButtonTapped: state.editScrum = nil return .none case .confirmEditButtonTapped: state.isPresentingEditView = false state.scrum = state.editingScrum if let scrum = state.editScrum?.scrum { state.scrum = scrum } state.editScrum = nil return .none /* ... */ case .editScrum: return .none } } .ifLet(\.$editScrum, action: \.editScrum) { DetailEditFeature() } } } struct DetailView: View { @Bindable var store: StoreOf<DetailFeature> var body: some View { List { /* ... */ } .navigationTitle(store.scrum.title) .toolbar { Button("Edit") { store.send(.editButtonTapped) } }
  113. @Reducer struct DetailFeature { @Observable State struct State: Equatable {

    @Shared var scrum: DailyScrum @Presents var editScrum: DetailEditFeature.State? } enum Action: BindableAction { /* ... */ case editScrum(PresentationAction<DetailEditFeature.Action>) } var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .editButtonTapped: state.editScrum = DetailEditFeature.State(scrum: state.scrum) return .none case .cancelEditButtonTapped: state.editScrum = nil return .none case .confirmEditButtonTapped: state.isPresentingEditView = false state.scrum = state.editingScrum if let scrum = state.editScrum?.scrum { state.scrum = scrum } state.editScrum = nil return .none /* ... */ case .editScrum: return .none } } .ifLet(\.$editScrum, action: \.editScrum) { DetailEditFeature() } } } struct DetailView: View { @Bindable var store: StoreOf<DetailFeature> var body: some View { List { /* ... */ } .navigationTitle(store.scrum.title) .toolbar { Button("Edit") {
  114. @Reducer struct DetailFeature { @Observable State struct State: Equatable {

    @Shared var scrum: DailyScrum @Presents var editScrum: DetailEditFeature.State? } enum Action: BindableAction { /* ... */ case editScrum(PresentationAction<DetailEditFeature.Action>) } var body: some ReducerOf<Self> { BindingReducer() Reduce { state, action in switch action { case .editButtonTapped: state.editScrum = DetailEditFeature.State(scrum: state.scrum) return .none case .cancelEditButtonTapped: state.editScrum = nil return .none case .confirmEditButtonTapped: state.isPresentingEditView = false state.scrum = state.editingScrum if let scrum = state.editScrum?.scrum { state.scrum = scrum } state.editScrum = nil return .none /* ... */ case .editScrum: return .none } } .ifLet(\.$editScrum, action: \.editScrum) { DetailEditFeature() } } } struct DetailView: View { @Bindable var store: StoreOf<DetailFeature> var body: some View { List { /* ... */ }
  115. } } struct DetailView: View { @Bindable var store: StoreOf<DetailFeature>

    var body: some View { List { /* ... */ } .navigationTitle(store.scrum.title) .toolbar { /* ... */ } .sheet(isPresented: $store.isPresentingEditView) { NavigationStack { DetailEditView(scrum: $store.editingScrum) .navigationTitle(store.scrum.title) .toolbar { /* ... */ } } } } } #Preview { NavigationStack { DetailView( store: Store( initialState: DetailFeature.State(scrum: Shared(DailyScrum.sampleData[0])) ) { DetailFeature() } ) } }
  116. } } struct DetailView: View { @Bindable var store: StoreOf<DetailFeature>

    var body: some View { List { /* ... */ } .navigationTitle(store.scrum.title) .toolbar { /* ... */ } .sheet(item: $store.scope(state: \.editScrum, action: \.editScrum)) { editScrumStore in NavigationStack { DetailEditView(store: editScrumStore) .navigationTitle(store.scrum.title) .toolbar { /* ... */ } } } } } #Preview { NavigationStack { DetailView( store: Store( initialState: DetailFeature.State(scrum: Shared(DailyScrum.sampleData[0])) ) { DetailFeature() } ) } }
  117. @MainActor func testAddScrum() async { var newScrum = DailyScrum(id: UUID(0),

    title: "Morning Sync", attendees: ["Alice", "Bob"], lengthInMinutes: 5, theme: .sky) let store = TestStore(initialState: ScrumsFeature.State(scrums: Shared([]))) { ScrumsFeature() } withDependencies: { $0.uuid = .constant(UUID(0)) } store.exhaustivity = .off await store.send(.newScrumButtonTapped) { $0.newScrum = NewScrumFeature.State(scrums: $0.$scrums) } await store.send(.newScrum(.presented(.editScrum(.binding(.set(\.scrum, newScrum)))))) { $0.newScrum?.editScrum.scrum = newScrum } await store.send(.newScrum(.presented(.confirmAddButtonTapped))) { $0.scrums = [newScrum] } await store.receive(\.newScrum.dismiss) { $0.newScrum = nil } }
  118. @MainActor func testAddScrum() async { var newScrum = DailyScrum(id: UUID(0),

    title: "Morning Sync", attendees: ["Alice", "Bob"], lengthInMinutes: 5, theme: .sky) let store = TestStore(initialState: ScrumsFeature.State(scrums: Shared([]))) { ScrumsFeature() } withDependencies: { $0.uuid = .constant(UUID(0)) } store.exhaustivity = .off await store.send(.newScrumButtonTapped) { $0.newScrum = NewScrumFeature.State(scrums: $0.$scrums) } await store.send(.newScrum(.presented(.editScrum(.binding(.set(\.scrum, newScrum)))))) { $0.newScrum?.editScrum.scrum = newScrum } await store.send(.newScrum(.presented(.confirmAddButtonTapped))) { $0.scrums = [newScrum] } await store.receive(\.newScrum.dismiss) { $0.newScrum = nil } }
  119. @MainActor func testAddScrum() async { var newScrum = DailyScrum(id: UUID(0),

    title: "Morning Sync", attendees: ["Alice", "Bob"], lengthInMinutes: 5, theme: .sky) let store = TestStore(initialState: ScrumsFeature.State(scrums: Shared([]))) { ScrumsFeature() } withDependencies: { $0.uuid = .constant(UUID(0)) } store.exhaustivity = .off await store.send(.newScrumButtonTapped) { $0.newScrum = NewScrumFeature.State(scrums: $0.$scrums) } await store.send(.newScrum(.presented(.editScrum(.binding(.set(\.scrum, newScrum)))))) { $0.newScrum?.editScrum.scrum = newScrum } await store.send(.newScrum(.presented(.confirmAddButtonTapped))) { $0.scrums = [newScrum] } await store.receive(\.newScrum.dismiss) { $0.newScrum = nil } }
  120. @MainActor func testAddScrum() async { var newScrum = DailyScrum(id: UUID(0),

    title: "Morning Sync", attendees: ["Alice", "Bob"], lengthInMinutes: 5, theme: .sky) let store = TestStore(initialState: ScrumsFeature.State(scrums: Shared([]))) { ScrumsFeature() } withDependencies: { $0.uuid = .constant(UUID(0)) } store.exhaustivity = .off await store.send(.newScrumButtonTapped) { $0.newScrum = NewScrumFeature.State(scrums: $0.$scrums) } await store.send(.newScrum(.presented(.editScrum(.binding(.set(\.scrum, newScrum)))))) { $0.newScrum?.editScrum.scrum = newScrum } await store.send(.newScrum(.presented(.confirmAddButtonTapped))) { $0.scrums = [newScrum] } await store.receive(\.newScrum.dismiss) { $0.newScrum = nil } }
  121. @Reducer struct NewScrumFeature { @ObservableState struct State: Equatable { var

    editScrum = DetailEditFeature.State(scrum: DailyScrum.emptyScrum) @Shared var scrums: IdentifiedArrayOf<DailyScrum> } enum Action: BindableAction { /* ... */ } @Dependency(\.dismiss) var dismiss var body: some ReducerOf<Self> { /* ... */ } } struct NewScrumSheet: View { /* ... */ } extension DailyScrum { static var emptyScrum: DailyScrum { @Dependency(\.uuid) var uuid return DailyScrum(id: UUID(), title: "", attendees: [], lengthInMinutes: 5, theme: .sky) } }
  122. @Reducer struct NewScrumFeature { @ObservableState struct State: Equatable { var

    editScrum = DetailEditFeature.State(scrum: DailyScrum.emptyScrum) @Shared var scrums: IdentifiedArrayOf<DailyScrum> } enum Action: BindableAction { /* ... */ } @Dependency(\.dismiss) var dismiss var body: some ReducerOf<Self> { /* ... */ } } struct NewScrumSheet: View { /* ... */ } extension DailyScrum { static var emptyScrum: DailyScrum { @Dependency(\.uuid) var uuid return DailyScrum(id: UUID(), title: "", attendees: [], lengthInMinutes: 5, theme: .sky) } }
  123. @Reducer struct NewScrumFeature { @ObservableState struct State: Equatable { var

    editScrum = DetailEditFeature.State(scrum: DailyScrum.emptyScrum) @Shared var scrums: IdentifiedArrayOf<DailyScrum> } enum Action: BindableAction { /* ... */ } @Dependency(\.dismiss) var dismiss var body: some ReducerOf<Self> { /* ... */ } } struct NewScrumSheet: View { /* ... */ } extension DailyScrum { static var emptyScrum: DailyScrum { @Dependency(\.uuid) var uuid return DailyScrum(id: uuid(), title: "", attendees: [], lengthInMinutes: 5, theme: .sky) } }
  124. @MainActor func testAddScrum() async { var newScrum = DailyScrum(id: UUID(0),

    title: "Morning Sync", attendees: ["Alice", "Bob"], lengthInMinutes: 5, theme: .sky) let store = TestStore(initialState: ScrumsFeature.State(scrums: Shared([]))) { ScrumsFeature() } withDependencies: { $0.uuid = .constant(UUID(0)) } store.exhaustivity = .off await store.send(.newScrumButtonTapped) { $0.newScrum = NewScrumFeature.State(scrums: $0.$scrums) } await store.send(\.newScrum.editScrum.binding.scrum, newScrum) { $0.newScrum?.editScrum.scrum = newScrum } await store.send(\.newScrum.confirmAddButtonTapped) { $0.scrums = [newScrum] } await store.receive(\.newScrum.dismiss) { $0.newScrum = nil } }
  125. @MainActor func testAddScrum() async { var newScrum = DailyScrum(id: UUID(0),

    title: "Morning Sync", attendees: ["Alice", "Bob"], lengthInMinutes: 5, theme: .sky) let store = TestStore(initialState: ScrumsFeature.State(scrums: Shared([]))) { ScrumsFeature() } withDependencies: { $0.uuid = .constant(UUID(0)) } store.exhaustivity = .off await store.send(.newScrumButtonTapped) { $0.newScrum = NewScrumFeature.State(scrums: $0.$scrums) } await store.send(\.newScrum.editScrum.binding.scrum, newScrum) { $0.newScrum?.editScrum.scrum = newScrum } await store.send(\.newScrum.confirmAddButtonTapped) { $0.scrums = [newScrum] } await store.receive(\.newScrum.dismiss) { $0.newScrum = nil } }
  126. @MainActor func testAddScrum() async { var newScrum = DailyScrum(id: UUID(0),

    title: "Morning Sync", attendees: ["Alice", "Bob"], lengthInMinutes: 5, theme: .sky) let store = TestStore(initialState: ScrumsFeature.State(scrums: Shared([]))) { ScrumsFeature() } withDependencies: { $0.uuid = .constant(UUID(0)) } store.exhaustivity = .off await store.send(.newScrumButtonTapped) { $0.newScrum = NewScrumFeature.State(scrums: $0.$scrums) } await store.send(\.newScrum.editScrum.binding.scrum, newScrum) { $0.newScrum?.editScrum.scrum = newScrum } await store.send(\.newScrum.confirmAddButtonTapped) { $0.scrums = [newScrum] } await store.receive(\.newScrum.dismiss) { $0.newScrum = nil } }
  127. @MainActor func testAddScrum() async { var newScrum = DailyScrum(id: UUID(0),

    title: "Morning Sync", attendees: ["Alice", "Bob"], lengthInMinutes: 5, theme: .sky) let store = TestStore(initialState: ScrumsFeature.State(scrums: Shared([]))) { ScrumsFeature() } withDependencies: { $0.uuid = .constant(UUID(0)) } store.exhaustivity = .off await store.send(.newScrumButtonTapped) { $0.newScrum = NewScrumFeature.State(scrums: $0.$scrums) } await store.send(\.newScrum.editScrum.binding.scrum, newScrum) { $0.newScrum?.editScrum.scrum = newScrum } await store.send(\.newScrum.confirmAddButtonTapped) { $0.scrums = [newScrum] } await store.receive(\.newScrum.dismiss) { $0.newScrum = nil } }
  128. @MainActor func testAddScrum() async { var newScrum = DailyScrum(id: UUID(0),

    title: "Morning Sync", attendees: ["Alice", "Bob"], lengthInMinutes: 5, theme: .sky) let store = TestStore(initialState: ScrumsFeature.State(scrums: Shared([]))) { ScrumsFeature() } withDependencies: { $0.uuid = .constant(UUID(0)) } store.exhaustivity = .off await store.send(.newScrumButtonTapped) { $0.newScrum = NewScrumFeature.State(scrums: $0.$scrums) } await store.send(\.newScrum.editScrum.binding.scrum, newScrum) { $0.newScrum?.editScrum.scrum = newScrum } await store.send(\.newScrum.confirmAddButtonTapped) { $0.scrums = [newScrum] } await store.receive(\.newScrum.dismiss) { $0.newScrum = nil } }
  129. @MainActor func testAddScrum() async { var newScrum = DailyScrum(id: UUID(0),

    title: "Morning Sync", attendees: ["Alice", "Bob"], lengthInMinutes: 5, theme: .sky) let store = TestStore(initialState: ScrumsFeature.State(scrums: Shared([]))) { ScrumsFeature() } withDependencies: { $0.uuid = .constant(UUID(0)) } store.exhaustivity = .off await store.send(.newScrumButtonTapped) { $0.newScrum = NewScrumFeature.State(scrums: $0.$scrums) } await store.send(\.newScrum.editScrum.binding.scrum, newScrum) { $0.newScrum?.editScrum.scrum = newScrum } await store.send(\.newScrum.confirmAddButtonTapped) { $0.scrums = [newScrum] } await store.receive(\.newScrum.dismiss) { $0.newScrum = nil } }
  130. @MainActor func testAddScrum() async { var newScrum = DailyScrum(id: UUID(0),

    title: "Morning Sync", attendees: ["Alice", "Bob"], lengthInMinutes: 5, theme: .sky) let store = TestStore(initialState: ScrumsFeature.State(scrums: Shared([]))) { ScrumsFeature() } withDependencies: { $0.uuid = .constant(UUID(0)) } store.exhaustivity = .off await store.send(.newScrumButtonTapped) { $0.newScrum = NewScrumFeature.State(scrums: $0.$scrums) } await store.send(\.newScrum.editScrum.binding.scrum, newScrum) { $0.newScrum?.editScrum.scrum = newScrum } await store.send(\.newScrum.confirmAddButtonTapped) { $0.scrums = [newScrum] } await store.receive(\.newScrum.dismiss) { $0.newScrum = nil } }
  131. @MainActor func testAddScrum() async { var newScrum = DailyScrum(id: UUID(0),

    title: "Morning Sync", attendees: ["Alice", "Bob"], lengthInMinutes: 5, theme: .sky) let store = TestStore(initialState: ScrumsFeature.State(scrums: Shared([]))) { ScrumsFeature() } withDependencies: { $0.uuid = .constant(UUID(0)) } store.exhaustivity = .off await store.send(.newScrumButtonTapped) { $0.newScrum = NewScrumFeature.State(scrums: $0.$scrums) } await store.send(\.newScrum.editScrum.binding.scrum, newScrum) { $0.newScrum?.editScrum.scrum = newScrum } await store.send(\.newScrum.confirmAddButtonTapped) { $0.scrums = [newScrum] } await store.receive(\.newScrum.dismiss) { $0.newScrum = nil } }
  132. Reference • try! Swift 2024 Composable Architecture workshop https://github.com/pointfreeco/TrySyncUps •

    The Composable Architecture Tutorials https://pointfreeco.github.io/swift-composable- architecture/main/tutorials/meetcomposablearchitecture/ • iOS 開發配飯吃 • SwiftUI + TCA 系列 https://www.youtube.com/@ethanhuang13/playlists Follow 「iamhands0me」on