코드 전문
import ComposableArchitecture
import SwiftUI
private let readMe = """
This case study demonstrates how to use shared data in order to implement a complex sign up flow.
The sign up flow consists of 3 steps, each of which can mutate a bit of shared data, and a final \
summary screen. The summary screen also allows the user to make any last minute edits to any of \
the data in the previous steps.
"""
struct SignUpData: Equatable {
var email = ""
var firstName = ""
var lastName = ""
var password = ""
var passwordConfirmation = ""
var phoneNumber = ""
var topics: Set<Topic> = []
enum Topic: String, Identifiable, CaseIterable {
case advancedSwift = "Advanced Swift"
case composableArchitecture = "Composable Architecture"
case concurrency = "Concurrency"
case modernSwiftUI = "Modern SwiftUI"
case swiftUI = "SwiftUI"
case testing = "Testing"
var id: Self { self }
}
}
@Reducer
private struct SignUpFeature {
@Reducer
enum Path {
case basics(BasicsFeature)
case personalInfo(PersonalInfoFeature)
case summary(SummaryFeature)
case topics(TopicsFeature)
}
@ObservableState
struct State {
var path = StackState<Path.State>()
@Shared var signUpData: SignUpData
}
enum Action {
case path(StackActionOf<Path>)
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .path(.element(id: _, action: .topics(.delegate(.stepFinished)))):
state.path.append(.summary(SummaryFeature.State(signUpData: state.$signUpData)))
return .none
case .path:
return .none
}
}
.forEach(\.path, action: \.path)
}
}
struct SignUpFlow: View {
@Bindable private var store = Store(
initialState: SignUpFeature.State(signUpData: Shared(SignUpData()))
) {
SignUpFeature()
}
var body: some View {
NavigationStack(path: $store.scope(state: \.path, action: \.path)) {
Form {
Section {
Text(readMe)
}
Section {
NavigationLink(
"Sign up",
state: SignUpFeature.Path.State.basics(
BasicsFeature.State(signUpData: store.$signUpData)
)
)
}
}
.navigationTitle("Sign up")
} destination: { store in
switch store.case {
case let .basics(store):
BasicsStep(store: store)
case let .personalInfo(store):
PersonalInfoStep(store: store)
case let .summary(store):
SummaryStep(store: store)
case let .topics(store):
TopicsStep(store: store)
}
}
}
}
@Reducer
private struct BasicsFeature {
@ObservableState
struct State {
var isEditingFromSummary = false
@Shared var signUpData: SignUpData
}
enum Action: BindableAction {
case binding(BindingAction<State>)
}
var body: some ReducerOf<Self> {
BindingReducer()
}
}
private struct BasicsStep: View {
@Environment(\.dismiss) private var dismiss
@Bindable var store: StoreOf<BasicsFeature>
var body: some View {
Form {
Section {
TextField("Email", text: $store.signUpData.email)
}
Section {
SecureField("Password", text: $store.signUpData.password)
SecureField("Password confirmation", text: $store.signUpData.passwordConfirmation)
}
}
.navigationTitle("Basics")
.toolbar {
ToolbarItem {
if store.isEditingFromSummary {
Button("Done") {
dismiss()
}
} else {
NavigationLink(
state: SignUpFeature.Path.State.personalInfo(
PersonalInfoFeature.State(signUpData: store.$signUpData)
)
) {
Text("Next")
}
}
}
}
}
}
@Reducer
private struct PersonalInfoFeature {
@ObservableState
struct State {
var isEditingFromSummary = false
@Shared var signUpData: SignUpData
}
enum Action: BindableAction {
case binding(BindingAction<State>)
}
@Dependency(\.dismiss) var dismiss
var body: some ReducerOf<Self> {
BindingReducer()
}
}
private struct PersonalInfoStep: View {
@Environment(\.dismiss) private var dismiss
@Bindable var store: StoreOf<PersonalInfoFeature>
var body: some View {
Form {
Section {
TextField("First name", text: $store.signUpData.firstName)
TextField("Last name", text: $store.signUpData.lastName)
TextField("Phone number", text: $store.signUpData.phoneNumber)
}
}
.navigationTitle("Personal info")
.toolbar {
ToolbarItem {
if store.isEditingFromSummary {
Button("Done") {
dismiss()
}
} else {
NavigationLink(
"Next",
state: SignUpFeature.Path.State.topics(
TopicsFeature.State(topics: store.$signUpData.topics)
)
)
}
}
}
}
}
@Reducer
private struct TopicsFeature {
@ObservableState
struct State {
@Presents var alert: AlertState<Never>?
var isEditingFromSummary = false
@Shared var topics: Set<SignUpData.Topic>
}
enum Action: BindableAction {
case alert(PresentationAction<Never>)
case binding(BindingAction<State>)
case delegate(Delegate)
case doneButtonTapped
case nextButtonTapped
enum Delegate {
case stepFinished
}
}
@Dependency(\.dismiss) var dismiss
var body: some ReducerOf<Self> {
BindingReducer()
Reduce { state, action in
switch action {
case .alert:
return .none
case .binding:
return .none
case .delegate:
return .none
case .doneButtonTapped:
if state.topics.isEmpty {
state.alert = AlertState {
TextState("Please choose at least one topic.")
}
return .none
} else {
return .run { _ in await dismiss() }
}
case .nextButtonTapped:
if state.topics.isEmpty {
state.alert = AlertState {
TextState("Please choose at least one topic.")
}
return .none
} else {
return .send(.delegate(.stepFinished))
}
}
}
.ifLet(\.alert, action: \.alert)
}
}
private struct TopicsStep: View {
@Bindable var store: StoreOf<TopicsFeature>
var body: some View {
Form {
Section {
Text("Please choose all the topics you are interested in.")
}
Section {
ForEach(SignUpData.Topic.allCases) { topic in
Toggle(isOn: $store.topics[contains: topic]) {
Text(topic.rawValue)
}
}
}
}
.navigationTitle("Topics")
.alert($store.scope(state: \.alert, action: \.alert))
.toolbar {
ToolbarItem {
if store.isEditingFromSummary {
Button("Done") {
store.send(.doneButtonTapped)
}
} else {
Button("Next") {
store.send(.nextButtonTapped)
}
}
}
}
.interactiveDismissDisabled()
}
}
@Reducer
private struct SummaryFeature {
@Reducer
enum Destination {
case alert(AlertState<Never>)
case basics(BasicsFeature)
case personalInfo(PersonalInfoFeature)
case topics(TopicsFeature)
}
@ObservableState
struct State {
@Presents var destination: Destination.State?
@Shared var signUpData: SignUpData
}
enum Action {
case destination(PresentationAction<Destination.Action>)
case editFavoriteTopicsButtonTapped
case editPersonalInfoButtonTapped
case editRequiredInfoButtonTapped
case submitButtonTapped
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .destination:
return .none
case .editFavoriteTopicsButtonTapped:
state.destination = .topics(
TopicsFeature.State(
isEditingFromSummary: true,
topics: state.$signUpData.topics
)
)
return .none
case .editPersonalInfoButtonTapped:
state.destination = .personalInfo(
PersonalInfoFeature.State(
isEditingFromSummary: true,
signUpData: state.$signUpData
)
)
return .none
case .editRequiredInfoButtonTapped:
state.destination = .basics(
BasicsFeature.State(
isEditingFromSummary: true,
signUpData: state.$signUpData
)
)
return .none
case .submitButtonTapped:
state.destination = .alert(
AlertState {
TextState("Thank you for signing up!")
}
)
return .none
}
}
.ifLet(\.$destination, action: \.destination)
}
}
private struct SummaryStep: View {
@Bindable var store: StoreOf<SummaryFeature>
var body: some View {
Form {
Section {
Text(store.signUpData.email)
Text(String(repeating: "•", count: store.signUpData.password.count))
} header: {
HStack {
Text("Required info")
Spacer()
Button("Edit") {
store.send(.editRequiredInfoButtonTapped)
}
.font(.caption)
}
}
Section {
Text(store.signUpData.firstName)
Text(store.signUpData.lastName)
Text(store.signUpData.phoneNumber)
} header: {
HStack {
Text("Personal info")
Spacer()
Button("Edit") {
store.send(.editPersonalInfoButtonTapped)
}
.font(.caption)
}
}
Section {
ForEach(store.signUpData.topics.sorted(by: { $0.rawValue < $1.rawValue })) { topic in
Text(topic.rawValue)
}
} header: {
HStack {
Text("Favorite topics")
Spacer()
Button("Edit") {
store.send(.editFavoriteTopicsButtonTapped)
}
.font(.caption)
}
}
Section {
Button {
store.send(.submitButtonTapped)
} label: {
Text("Submit")
}
}
}
.navigationTitle("Summary")
.sheet(
item: $store.scope(state: \.destination?.basics, action: \.destination.basics)
) { basicsStore in
NavigationStack {
BasicsStep(store: basicsStore)
}
.presentationDetents([.medium])
}
.sheet(
item: $store.scope(state: \.destination?.personalInfo, action: \.destination.personalInfo)
) { personalStore in
NavigationStack {
PersonalInfoStep(store: personalStore)
}
.presentationDetents([.medium])
}
.sheet(
item: $store.scope(state: \.destination?.topics, action: \.destination.topics)
) { topicsStore in
NavigationStack {
TopicsStep(store: topicsStore)
}
.presentationDetents([.medium])
}
.alert($store.scope(state: \.destination?.alert, action: \.destination.alert))
}
}
#Preview("Sign up") {
SignUpFlow()
}
#Preview("Basics") {
NavigationStack {
BasicsStep(
store: Store(initialState: BasicsFeature.State(signUpData: Shared(SignUpData()))) {
BasicsFeature()
}
)
}
}
#Preview("Personal info") {
NavigationStack {
PersonalInfoStep(
store: Store(initialState: PersonalInfoFeature.State(signUpData: Shared(SignUpData()))) {
PersonalInfoFeature()
}
)
}
}
#Preview("Topics") {
NavigationStack {
TopicsStep(
store: Store(initialState: TopicsFeature.State(topics: Shared([]))) {
TopicsFeature()
}
)
}
}
#Preview("Summary") {
NavigationStack {
SummaryStep(
store: Store(
initialState: SummaryFeature.State(
signUpData: Shared(
SignUpData(
email: "blob@pointfree.co",
firstName: "Blob",
lastName: "McBlob",
password: "blob is awesome",
passwordConfirmation: "blob is awesome",
phoneNumber: "212-555-1234",
topics: [
.composableArchitecture,
.concurrency,
.modernSwiftUI,
]
)
)
)
) {
SummaryFeature()
}
)
}
}
extension Set {
fileprivate subscript(contains element: Element) -> Bool {
get { contains(element) }
set {
if newValue {
insert(element)
} else {
remove(element)
}
}
}
}
리드미 해석
This case study demonstrates how to use shared data in order to implement a complex sign up flow.
The sign up flow consists of 3 steps, each of which can mutate a bit of shared data, and a final summary screen. The summary screen also allows the user to make any last minute edits to any of the data in the previous steps.
이 사례 연구에서는 복잡한 가입 절차를 구현하기 위해 공유 데이터를 사용하는 방법을 보여줍니다.
가입 절차는 3단계로 구성되며, 각 단계마다 공유 데이터를 약간씩 변경할 수 있고 최종 요약 화면이 있습니다. 요약 화면에서는 사용자가 이전 단계의 데이터를 마지막으로 수정할 수도 있습니다.
@Shared State
TCA 1.10 버전부터 SharedState라는 기능이 생겼습니다. 이는 각 화면에서 사용하는 객체들을 ReferenceType"처럼"보여주고 사용하는데에 사용할 수 있습니다.
부모 뷰에서 State에 Shared로 선언된 Property를 state.$property로 자식 뷰의 State에 넘길 수 있습니다.
struct State {
var path = StackState<Path.State>()
@Shared var signUpData: SignUpData
}
state.path.append(.summary(SummaryFeature.State(signUpData: state.$signUpData)))
게시물은 TCA 라이브러리의 CaseStudies를 직접 작성해보고 어떻게 작동하는지에 대해서 공부하기 위해서 작성했습니다.
'Swift > TCA' 카테고리의 다른 글
[TCA] TCA 1.10 이후부터의 Shared State의 initRule (0) | 2024.05.02 |
---|---|
[TCA] day 9 Shared State활용한 TodosList 예제 (TCA @Shared, Sheet) (0) | 2024.05.01 |
[TCA] day 7 TCA의 Dependency에 대해서 (Effects-Basics) (0) | 2024.04.29 |
[TCA] day 5 Optional State를 활용하여 View조작하기(GettingStarted-OptionalState) (0) | 2024.04.26 |
[TCA] day 4 BindableAction Portocol을 활용하여 Binding 하기(GettingStarted-Bindings-Forms) (0) | 2024.04.24 |