[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 |