안녕하세요. 이번 시간은 Navigation Tutorial의 두번째 장인 Multiple presentation destinations에 대해 알아보겠습니다. 이번 튜토리얼은 통해 여러분들은 부모 피처에서 자식 피처에게 많은 기능을 표현하려면 어떻게 하는지 배울 수 있게 됩니다.
연락처 목록에 연락처를 삭제할 수 있는 기능을 추가하되, 진짜 삭제를 할 것인지 확인해야합니다. 저번 시간에 배웠던 Presents(). PresentationAction() 및 ifLet을 사용하여 alert도 구현할 수 있습니다.
@Reducer
struct ContactsFeature {
@ObservableState
struct State: Equatable {
@Presents var addContact: AddContactFeature.State?
var contacts: IdentifiedArrayOf<Contact> = []
}
enum Action {
case addButtonTapped
case addContact(PresentationAction<AddContactFeature.Action>)
case deleteButtonTapped(id: Contact.ID)
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .addButtonTapped:
state.addContact = AddContactFeature.State(
contact: Contact(id: UUID(), name: "")
)
return .none
case let .addContact(.presented(.delegate(.saveContact(contact)))):
state.contacts.append(contact)
return .none
case .addContact:
return .none
case let .deleteButtonTapped(id: id):
return .none
}
}
.ifLet(\.$addContact, action: \.addContact) {
AddContactFeature()
}
}
}
지난 챕터에서 작업했던 ContactsFeature.swift로 돌아가서 삭제 action에 대한 새로운 코드를 추가합니다. 지금 상태는 삭제 action은 추가한 상태이지만 삭제를 확인할 alert은 없는 상태입니다. 아래 작업을 통해 Alert도 추가해줍시다.
@ObservableState
struct State: Equatable {
@Presents var addContact: AddContactFeature.State?
@Presents var alert: AlertState<Action.Alert>?
var contacts: IdentifiedArrayOf<Contact> = []
}
enum Action {
case addButtonTapped
case addContact(PresentationAction<AddContactFeature.Action>)
case alert(PresentationAction<Alert>)
case deleteButtonTapped(id: Contact.ID)
enum Alert: Equatable {
case confirmDeletion(id: Contact.ID)
}
}
@Presents를 통해 AlertState를 추가해줍니다. 참고로 AlertState는 Equtable을 준수합니다. 이후 alert 알림창을 구현해봅시다.
case let .deleteButtonTapped(id: id):
state.alert = AlertState {
TextState("Are you sure?")
} actions: {
ButtonState(role: .destructive, action: .confirmDeletion(id: id)) {
TextState("Delete")
}
}
deleteButtonTapped 액션이 들어오면 위와같은 알림창을 띄우고 Delete를 누르면 다시 confirmDeletion 액션으로 넘어갑니다.
코드 하단에 ifLet을 추가해줍니다.
.ifLet(\.$alert, action: \.alert)
이후 alert action을 구현해줍니다.
case let .alert(.presented(.confirmDeletion(id: id))):
state.contacts.remove(id: id)
return .none
case .alert:
return .none
우리는 위에서 redeucer에 대한 수정을 가했습니다. 실제로 사용자에게 보여지는 view 또한 수정해줍니다.
.alert($store.scope(state: \.alert, action: \.alert))
마지막으로, View에도 삭제 버튼을 추가해줘야겠죠? 다음과 같이 코드를 수정해봅시다.
ForEach(store.contacts) { contact in
HStack {
Text(contact.name)
Spacer()
Button {
store.send(.deleteButtonTapped(id: contact.id))
} label: {
Image(systemName: "trash")
.foregroundColor(.red)
}
}
}
현재 ContactsFeature는 두 목적지로 이동할 수 있습니다. Add Contacts sheet 혹은 delete alert인데요. 중요한 점은 두 목적지를 동시에 이동할 수 없다는 것입니다. 하지만, Presents()를 이용하면 가능합니다. navigate를 위해 optional을 사용하면 navigate 수에 따라 optional의 수가 늘어나게 됩니다. 2개의 optional은 1개의 invalid가 있고, 4개의 optional은 3개의 invalid가 있고 4개의 optional에는 11개의 invalid가 있습니다.이러한 옵셔널은 application을 더 복잡하게 만듭니다. 이번 섹션에서는 보다 간결하게 개선시키는법을 알아봅시다.
extension ContactsFeature {
@Reducer
enum Destination {
}
}
extension ContactsFeature {
@Reducer
enum Destination {
case addContact(AddContactFeature)
}
}
extension ContactsFeature {
@Reducer
enum Destination {
case addContact(AddContactFeature)
case alert(AlertState<ContactsFeature.Action.Alert>)
}
}
@ObservableState
struct State: Equatable {
var contacts: IdentifiedArrayOf<Contact> = []
// @Presents var addContact: AddContactFeature.State?
// @Presents var alert: AlertState<Action.Alert>?
@Presents var destination: Destination.State?
}
extension ContactsFeature {
@Reducer
enum Destination {
case addContact(AddContactFeature)
case alert(AlertState<ContactsFeature.Action.Alert>)
}
}
extension ContactsFeature.Destination.State: Equatable {}
enum Action {
case addButtonTapped
case deleteButtonTapped(id: Contact.ID)
// case addContact(PresentationAction<AddContactFeature.Action>)
// case alert(PresentationAction<Alert>)
case destination(PresentationAction<Destination.Action>)
enum Alert: Equatable {
case confirmDeletion(id: Contact.ID)
}
}
case .addButtonTapped:
state.destination = .addContact(
AddContactFeature.State(
contact: Contact(id: UUID(), name: "")
)
)
return .none
case let .destination(.presented(.addContact(.delegate(.saveContact(contact))))):
state.contacts.append(contact)
return .none
case let .destination(.presented(.alert(.confirmDeletion(id: id)))):
state.contacts.remove(id: id)
return .none
case .destination:
return .none
case let .deleteButtonTapped(id: id):
state.destination = .alert(
AlertState {
TextState("Are you sure?")
} actions: {
ButtonState(role: .destructive, action: .confirmDeletion(id: id)) {
TextState("Delete")
}
}
)
return .none
}
@Reducer
struct ContactsFeature {
@ObservableState
struct State: Equatable {
var contacts: IdentifiedArrayOf<Contact> = []
@Presents var destination: Destination.State?
}
enum Action {
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))))):
state.contacts.append(contact)
return .none
case let .destination(.presented(.alert(.confirmDeletion(id: id)))):
state.contacts.remove(id: id)
return .none
case .destination:
return .none
case let .deleteButtonTapped(id: id):
state.destination = .alert(
AlertState {
TextState("Are you sure?")
} actions: {
ButtonState(role: .destructive, action: .confirmDeletion(id: id)) {
TextState("Delete")
}
}
)
return .none
}
}
.ifLet(\.$destination, action: \.destination)
}
}
.sheet(
item: $store.scope(state: \.destination?.addContact, action: \.destination.addContact)
) { addContactStore in
NavigationStack {
AddContactView(store: addContactStore)
}
}
.alert($store.scope(state: \.destination?.alert, action: \.destination.alert))
두 개의 독립적이고 부정확하게 모델링된 옵션 값을 하나의 옵션 열거로 변환하기만 하면 됩니다. 이제 한 번에 하나의 목적지만 활성화할 수 있음을 증명할 수 있습니다. 이제 남은 것은 뷰를 업데이트하여 목적지 열거가 시트를 구동하는 경우와 알림을 구동하는 경우를 지정할 수 있도록 하는 것뿐입니다.