apple/TCA

[TCA] Tutorial #5 (Multiple presentation destinations)

lgvv 2023. 9. 24. 12:12

 

[TCA] Tutorial #5 (Multiple presentation destinations)

 

// MARK: - Contact

import Foundation
import ComposableArchitecture

struct Contact: Equatable, Identifiable {
    let id: UUID
    var name: String
}

struct ContactsFeature: Reducer {
    struct State: Equatable {
        var contacts: IdentifiedArrayOf<Contact> = []
        @PresentationState var destination: Destination.State? // 화면전환을 이렇게 묶어서 분리해서 처리
    }
    enum Action: Equatable {
        case addButtonTapped
        case deleteButtonTapped(id: Contact.ID)
        case destination(PresentationAction<Destination.Action>) // 화면전환 액션
        enum Alert: Equatable {
            case confirmDeletion(id: Contact.ID)
        }
    }
    var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
            case .addButtonTapped:
                state.destination = .addContact(
                    AddContactFeature.State(
                        contact: Contact(id: UUID(), name: "")
                    )
                )
                return .none
                
            case let .destination(.presented(.addContact(.delegate(.saveContact(contact))))):
                // delegate를 통해서 다른 목적지의 action으로 연결
                state.contacts.append(contact)
                return .none
                
            case let .destination(.presented(.alert(.confirmDeletion(id: id)))):
                // alert를 연결
                state.contacts.remove(id: id)
                return .none
                
            case .destination:
                return .none
                
            case let .deleteButtonTapped(id: id):
                state.destination = .alert(
                    // AlertState 시스템 알럿에 cancel이 디폴트로 들어가 있음
                    AlertState {
                        TextState("Are you sure?")
                    } actions: {
                        ButtonState(role: .destructive, action: .confirmDeletion(id: id)) {
                            TextState("Delete")
                        }
                    }
                )
                return .none
            }
        }
        .ifLet(\.$destination, action: /Action.destination) {
            // 화면이동에 대한 리듀서를 결합
            Destination()
        }
    }
}

extension ContactsFeature {
    struct Destination: Reducer {
        // 화면이동에 대한 부분은 extension으로 처리
        enum State: Equatable {
            case addContact(AddContactFeature.State)
            case alert(AlertState<ContactsFeature.Action.Alert>)
        }
        enum Action: Equatable {
            case addContact(AddContactFeature.Action)
            case alert(ContactsFeature.Action.Alert)
        }
        var body: some ReducerOf<Self> {
            // 하나의 Reducer밖에 없어서 이렇게 처리
            // 알럿의 경우에는 별도로 Reducer가 필요없기 때문.
            Scope(state: /State.addContact, action: /Action.addContact) {
                AddContactFeature()
            }
        }
    }
}

import SwiftUI

struct ContactsView: View {
    let store: StoreOf<ContactsFeature>
    
    var body: some View {
        NavigationStack {
            WithViewStore(self.store, observe: \.contacts) { viewStore in
                List {
                    ForEach(viewStore.state) { contact in
                        HStack {
                            Text(contact.name)
                            Spacer()
                            Button {
                                viewStore.send(.deleteButtonTapped(id: contact.id))
                            } label: {
                                Image(systemName: "trash")
                                    .foregroundColor(.red)
                            }
                        }
                    }
                }
                .navigationTitle("Contacts")
                .toolbar {
                    ToolbarItem {
                        Button {
                            viewStore.send(.addButtonTapped)
                        } label: {
                            Image(systemName: "plus")
                        }
                    }
                }
            }
        }
        .sheet(store: self.store.scope(state: \.$destination, action: { .destination($0) }),
               state: /ContactsFeature.Destination.State.addContact, // case path로 사용하는데 이 문법은 정리가 필요할 듯 싶다.
               action: ContactsFeature.Destination.Action.addContact
        ) { addContactStore in
            NavigationStack {
                AddContactView(store: addContactStore)
            }
        }
        .alert(
            // 알럿도 동일하게 사용한다. action을 주고 후처리도 reducer에서 담당한다. View 로직이 매우 깔끔해짐
            store: self.store.scope(state: \.$destination, action: { .destination($0) }),
            state: /ContactsFeature.Destination.State.alert,
            action: ContactsFeature.Destination.Action.alert
        )
    }
}



import ComposableArchitecture


struct AddContactFeature: Reducer {
    struct State: Equatable {
        var contact: Contact
    }
    enum Action: Equatable {
        case cancelButtonTapped
        case delegate(Delegate) // 부모 자식간에 delegate로 연결
        case saveButtonTapped
        case setName(String)
        enum Delegate: Equatable {
            // 부모 자식간에 delegate로 연결
            // case cancel
            case saveContact(Contact)
        }
    }
    @Dependency(\.dismiss) var dismiss
    func reduce(into state: inout State, action: Action) -> Effect<Action> {
        switch action {
        case .cancelButtonTapped:
            // dismiss는 비동기적으로 수행되므로 Efeect에서 호출하는게 적절함.
            return .run { _ in await self.dismiss() }
            
        case .delegate:
            // 딜리게이트는 실제로 어떤 로직을 처리해서는 안된다. 반드시 .none!
            return .none
            
        case .saveButtonTapped:
            return .run { [contact = state.contact] send in
                await send(.delegate(.saveContact(contact))) // 딜리게이트로 전달!
                await self.dismiss()
            }
            
        case let .setName(name):
            state.contact.name = name
            return .none
        }
    }
}

struct AddContactView: View {
    let store: StoreOf<AddContactFeature>
    
    var body: some View {
        WithViewStore(self.store, observe: { $0 }) { viewStore in
            Form {
                TextField("Name", text: viewStore.binding(get: \.contact.name, send: { .setName($0) })) // binding하는 방법
                Button("Save") {
                    viewStore.send(.saveButtonTapped)
                }
            }
            .toolbar {
                ToolbarItem {
                    Button("Cancel") {
                        viewStore.send(.cancelButtonTapped)
                    }
                }
            }
        }
    }
}

 

 

'apple > TCA' 카테고리의 다른 글

[TCA] SharedState  (1) 2023.09.27
[TCA] OptionalState (IfLetCase)  (0) 2023.09.27
[TCA] FocusState  (0) 2023.09.27
[TCA] Binding  (0) 2023.09.27
[TCA] 공부기록 #1 (ReducerProtocol)  (0) 2023.01.16