Notice
Recent Posts
Recent Comments
Link
๊ด€๋ฆฌ ๋ฉ”๋‰ด

lgvv98

[TCA] Tutorial #5 (Multiple presentation destinations) ๋ณธ๋ฌธ

apple/๐Ÿฆฅ TCA

[TCA] Tutorial #5 (Multiple presentation destinations)

๐Ÿฅ• ์บ๋Ÿฟ๋งจ 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
Comments