안녕하세요. 이번 시간은 드디어 Meet the Composable Architecture 튜토리얼의 마지막 장인 Navigation stacks입니다. 제목에 나와있는 Navigation Stack은 iOS 16부터 신규 제공되능 기능인데요.
Navigation Stack document iOS 15까지의 Navigation은 Navigation View 로 사용하던것과 다른 개념이죠. 이에 대한 설명은 다음에 하도록 하겠습니다!
이번 시간은 위에서 나온 Composable Architecture와 Navigation Stacks의 조합을 알아봅시다!!
우선 ContactsView 에서 접근할 수 있는 ContactsDetailView와 ContactsDetailFeature를 만들어야합니다.
@Reducer
struct ContactDetailFeature {
@ObservableState
struct State: Equatable {
let contact: Contact
}
enum Action {
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
}
}
}
}
위와 같이 ContactsDetailFeature.swift 파일을 생성해줍니다.
import ComposableArchitecture
import SwiftUI
struct ContactDetailView: View {
let store: StoreOf<ContactDetailFeature>
var body: some View {
Form {
}
.navigationTitle(Text(store.contact.name))
}
}
#Preview {
NavigationStack {
ContactDetailView(
store: Store(
initialState: ContactDetailFeature.State(
contact: Contact(id: UUID(), name: "Blob")
)
) {
ContactDetailFeature()
}
)
}
}
마찬가지로 ContactsDetailView.swift 파일을 만들어줍니다.
Section 1에서 세부 연락처 Reducer와 뷰를 만들었으므로 ContactsFeature에서 Detail로 드릴다운 할 수 있게 됩니다.
@ObservableState
struct State: Equatable {
var contacts: IdentifiedArrayOf<Contact> = []
@Presents var destination: Destination.State?
var path = StackState<ContactDetailFeature.State>()
}
StackState 는 스택에 푸시되고 있는 기능을 나타냅니다.
enum Action {
case addButtonTapped
case deleteButtonTapped(id: Contact.ID)
case destination(PresentationAction<Destination.Action>)
case path(StackAction<ContactDetailFeature.State, ContactDetailFeature.Action>)
enum Alert: Equatable {
case confirmDeletion(id: Contact.ID)
}
}
StackAction은 스택 내부에서 element를 push하거나 pop하는 등의 작업이나 스택 내부 기능에서 발생할 수 있는 작업을 나타냅니다.
.forEach(\.path, action: \.path) {
ContactDetailFeature()
}
ForEach 를 통해 ContactDetailFeature를 ContactFeature의 스택에 통합할 수 있습니다.
NavigationStack(path: $store.scope(state: \.path, action: \.path)) {
NavigationStack을 통해 StackState와 StackAction으로 범위가 지정단 store에 바인딩 됩니다.
destination: { store in
ContactDetailView(store: store)
}
ForEach(store.contacts) { contact in
NavigationLink(state: ContactDetailFeature.State(contact: contact)) {
HStack {
Text(contact.name)
Spacer()
Button {
store.send(.deleteButtonTapped(id: contact.id))
} label: {
Image(systemName: "trash")
.foregroundColor(.red)
}
}
}
.buttonStyle(.borderless)
}
}
init과 관련된 document
지금까지 작업을 통해 우리는 ContactsView에 AddContactsView를 연결했지만 뷰 간의 상호 작용은 없습니다. 이번 섹션에서 연락처를 삭제할 수 있는 기능을 추가해 보도록 하겠습니다.
@ObservableState
struct State: Equatable {
@Presents var alert: AlertState<Action.Alert>?
let contact: Contact
}
enum Action {
case alert(PresentationAction<Alert>)
case delegate(Delegate)
case deleteButtonTapped
enum Alert {
case confirmDeletion
}
enum Delegate {
case confirmDeletion
}
}
사용자가 UI에서 할 수 있는 모든 작업, 예를 들어 "삭제" 버튼 탭하기와 같은 작업뿐만 아니라 알림 내부의 모든 작업과 부모 기능에 연락하여 연락처를 삭제하도록 지시해야 할 때의 위임 작업도 포함됩니다.
알림 및 위임 작업에는 이전에 필요했던 것처럼 ID가 필요하지 않습니다. 그 이유는 곧 알게 될 것입니다.
@Dependency(\.dismiss) var dismiss
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .alert(.presented(.confirmDeletion)):
return .run { send in
await send(.delegate(.confirmDeletion))
await self.dismiss()
}
case .alert:
return .none
case .delegate:
return .none
case .deleteButtonTapped:
state.alert = .confirmDeletion
return .none
}
}
.ifLet(\.$alert, action: \.alert)
}
}
extension AlertState where Action == ContactDetailFeature.Action.Alert {
static let confirmDeletion = Self {
TextState("Are you sure?")
} actions: {
ButtonState(role: .destructive, action: .confirmDeletion) {
TextState("Delete")
}
}
}
본문 속성에 새로운 작업을 구현합니다. 이는 여러 프레젠테이션 대상에서 수행한 작업과 매우 유사합니다. 삭제 확인은 위임 작업을 보내고 DismissEffect 종속성을 사용하여 무시하는 방식으로 처리하며, 나중에 테스트하기 쉽도록 자체 도우미에게 경고 상태를 추출했습니다.
var body: some View {
Form {
Button("Delete") {
store.send(.deleteButtonTapped)
}
}
.navigationTitle(Text(store.contact.name))
.alert($store.scope(state: \.alert, action: \.alert))
}
case let .path(.element(id: id, action: .delegate(.confirmDeletion))):
guard let detailState = state.path[id: id]
else { return .none }
state.contacts.remove(id: detailState.contact.id)
return .none
특히 .delegate(.confirmDeletion) 작업이 언제 전송되는지 듣고, 이 경우 배열에서 연락처를 제거하고자 합니다.
이를 마지막으로 우린 간단한 튜토리얼을 모두 완료했습니다.
고생하셨습니다 :)