작성개요
TCA 1.10부터 SharedState을 관리하는 방식이 달라졌습니다. 이전에 SharedState를 관리하기 위해서 onchange매서드를 통해관리했는데, 이제는 @Shared라는 Macro로 관리하는 방법으로 달라졌습니다. 그래서 이를 공부하고자 글을 작성하게 되었습니다.
코드
MakingFrogWithoutDissecting/TIL_TodosWithTCAShared/TIL_TodosWithTCAShared at main · MaraMincho/MakingFrogWithoutDissecting
개구리를 해부하지 않고 직접 만들기, 공부 레포. Contribute to MaraMincho/MakingFrogWithoutDissecting development by creating an account on GitHub.
github.com
//
// ContentView.swift
// TIL_TodosWithTCAShared
//
// Created by MaraMincho on 5/1/24.
//
import SwiftUI
import ComposableArchitecture
struct TodosMainView: View {
@Bindable
var store: StoreOf<TodosMain>
var body: some View {
VStack {
Text("최근 편집한 TODO: \(store.recentEdited.title)")
ZStack {
Color
.green
.frame(height: 60)
.clipShape(RoundedRectangle(cornerRadius: 8))
HStack {
Text("투두 투두 ")
Spacer()
Button("추가") {
store.send(.tappedCreateTodo)
}
}
.padding()
}
ScrollView() {
VStack {
ForEach(store.todosContent) { content in
ZStack {
Color
.black
.opacity(0.1)
.frame(height: 60)
.clipShape(RoundedRectangle(cornerRadius: 8))
HStack {
Text(content.title)
.onTapGesture {
store.send(.tappedDetailOfTodos(id: content.id))
}
Spacer()
Button {
store.send(.deleteTodo(id: content.id))
} label: {
Image(systemName: "pencil")
}
}
.frame(maxWidth: .infinity)
}
}
}
}
}
.sheet(item: $store.scope(state: \.todo, action: \.todo)) { store in
TodoView(store: store)
}
.navigationTitle("투두둑 투두둑")
.padding()
}
}
struct TodoContentProperty: Equatable, Identifiable {
var title: String
var content: String
var id = UUID()
}
@Reducer
struct TodosMain {
@ObservableState
struct State {
@Shared var todosContent: [TodoContentProperty]
@Presents var todo: Todo.State? = nil
@Shared var recentEdited: TodoContentProperty
init() {
_recentEdited = .init(TodoContentProperty(title: "", content: ""))
_todosContent = .init([])
}
}
enum Action: Equatable {
case tappedCreateTodo
case tappedDetailOfTodos(id: UUID)
case presentTodo(id: UUID)
case todo(PresentationAction<Todo.Action>)
case deleteTodo(id: UUID)
}
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .tappedCreateTodo:
let newTodo = TodoContentProperty(title: "", content: "")
state.recentEdited = newTodo
state.todo = .init(todo: state.$recentEdited)
return .none
case let .tappedDetailOfTodos(id):
return .run { send in
await send(.presentTodo(id: id))
}
case let .presentTodo(id):
if let ind = state.todosContent.enumerated().first(where: {$0.element.id == id})?.offset{
state.recentEdited = state.todosContent[ind]
state.todo = .init(todo: state.$recentEdited)
}
return .none
case .todo(.dismiss):
if let index = state.todosContent.enumerated().first(where: {$0.element.id == state.recentEdited.id})?.offset {
state.todosContent[index] = state.recentEdited
}else {
state.todosContent.append(state.recentEdited)
}
return .none
case .todo:
return .none
case let .deleteTodo(id):
state.todosContent = state.todosContent.filter{$0.id != id}
return .none
}
}
.ifLet(\.$todo, action: \.todo) {
Todo()
}
}
}
//
// ContentView.swift
// TIL_TodosWithTCAShared
//
// Created by MaraMincho on 5/1/24.
//
import SwiftUI
import ComposableArchitecture
struct TodoView: View {
@Bindable
var store: StoreOf<Todo>
var body: some View {
VStack {
TextField("", text: $store.todo.title, prompt: Text("제목을 입력하세요"))
Color.gray
.frame(maxWidth: .infinity, maxHeight: 4)
ZStack {
Color
.black
.opacity(0.1)
.frame(maxHeight: .infinity)
.clipShape(RoundedRectangle(cornerRadius: 8))
TextField("", text: $store.todo.content, axis: .vertical)
.frame(maxHeight: .infinity)
}
}
.padding()
}
}
@Reducer
struct Todo {
@ObservableState
struct State {
@Shared var todo: TodoContentProperty
init(todo: Shared<TodoContentProperty>) {
_todo = todo
}
}
enum Action: BindableAction, Equatable {
case binding(BindingAction<Todo.State>)
case tappedCreateTodo
case tappedDetailOfTodos
}
var body: some Reducer<State, Action> {
BindingReducer()
Reduce { state, action in
switch action {
default:
return .none
}
}
}
}
동작 화면
코드 설명
@Shared를 자식에게 전달
SharedState가 쓰이는 곳은 Modal하는 부분입니다. Modal하는 부분은 ChildState입니다. Parent State에서 ReferenceType으로 인자를 넘겨준다면 좋겠지만, TCA특성상 Class를 넘겨도 깊은 복사가 이루어집니다. 그렇기에 @Shared를 활용하여 현재 펼쳐진 Todo에 대해서 연결해주면 좋을 것 같습니다.
struct State {
@Shared var todosContent: [TodoContentProperty]
@Presents var todo: Todo.State? = nil
@Shared var recentEdited: TodoContentProperty
//중요
init() {
_recentEdited = .init(TodoContentProperty(title: "", content: ""))
_todosContent = .init([])
}
}
enum Action: Equatable {
case tappedCreateTodo
case tappedDetailOfTodos(id: UUID)
case presentTodo(id: UUID)
case todo(PresentationAction<Todo.Action>)
case deleteTodo(id: UUID)
}
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .tappedCreateTodo:
let newTodo = TodoContentProperty(title: "", content: "")
state.recentEdited = newTodo
state.todo = .init(todo: state.$recentEdited)
return .none
// code...
부모 View에서 자식 View로 레퍼런스를 줘야 합니다. 우리는 이를 @shared 로 선언된 recentEdited Property로 넘길 예정입니다. 만약 tappedCreateTodo가 실행될 경우 새 프로퍼티를 생성하고 이를 recentEdited 에 할당합니다. 그리고 이 shared객체를 하위 모달뷰의 state에 넘겨줍니다.(초기값을 세팅)
이를 그림으로 나타내면 다음과 같습니다.
Action을 통해 TodoReducer를 생성하고 TodoReducerState에 arguement인 TodoProperty를 부모 View recentEdited로 넘겨줍니다. 그렇게 되어서 ifletd을 통해 TodoReducer를 생성하게 됩니다.
TodoReducer을 통해서 생성하면 @shared로 선언했던 Property들이 참조 관계가 될 것입니다.
@Shared로 부모뷰의 SharingState
그리고 TodoReducer의 Title이나 Content들이 바뀔 때 마다 부모View의 state에 즉각적으로 변경될 것입니다. TodoReducer를 생성하기 위해서 선언한 State의 매개변수가 참조관계이기 때문입니다. 우리는 자식 Reducer가 dismiss되었을 때 관리해주면 됩니다.
관리는 두가지 분기로 나뉩니다.
1. state.todosContent에 recentEditedProperty이미 있을 경우(편집)
편집의 경우는 Content를 업데이트 시켜주면 됩니다.
2. state.todosContent에 recentEditedProperty 없을 경우(생성)
생성의 경우여서, todos에 값을 업데이트 시켜줍니다.
case .todo(.dismiss):
if let index = state.todosContent.enumerated().first(where: {$0.element.id == state.recentEdited.id})?.offset {
state.todosContent[index] = state.recentEdited
}else {
state.todosContent.append(state.recentEdited)
}
return .none
tree based navigation(현재는 Modal)
TCA에서 Modal을 구현하는 방법은 여러가지가 있습니다. modal을 구현하기 위해서 child.state가 parent.state에 선언되어 있어야 합니다. 그리고 특정 액션시 state.childState = .Init(...) 으로 값을 주입해서 활용하면됩니다.
이는 공식문서 코드에 잘 나와있습니다. state.addItem = itemFromFeature.state()로 state을 주입하고, iflet분기를 통해서 reducer를 생성합니다.
@Reducer
struct InventoryFeature {
@ObservableState
struct State: Equatable { /* ... */ }
enum Action { /* ... */ }
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .addButtonTapped:
// Populating this state performs the navigation
state.addItem = ItemFormFeature.State()
return .none
// ...
}
}
.ifLet(\.$addItem, action: \.addItem) {
ItemFormFeature()
}
}
}
그리고 View에서 .sheet(item:)으로 적절한 store의 scope을 넘기는 모습을 확인할 수 있습니다. 원하는 scope을 통해서 reducer가 있을 경우 뷰를 생성합니다.
struct InventoryView: View {
@Bindable var store: StoreOf<InventoryFeature>
var body: some View {
List {
// ...
}
.sheet(
item: $store.scope(state: \.addItem, action: \.addItem)
) { store in
ItemFormView(store: store)
}
}
}
참고자료
Documentation
pointfreeco.github.io
Documentation
pointfreeco.github.io
'Swift > TCA' 카테고리의 다른 글
[TCA] day 10 Effect 취소 (TCA @Shared, Sheet) (0) | 2024.05.03 |
---|---|
[TCA] TCA 1.10 이후부터의 Shared State의 initRule (0) | 2024.05.02 |
[TCA] day 8 TCA 1.10 이후부터의 Shared State 관리(@Shared) (0) | 2024.04.30 |
[TCA] day 7 TCA의 Dependency에 대해서 (Effects-Basics) (0) | 2024.04.29 |
[TCA] day 5 Optional State를 활용하여 View조작하기(GettingStarted-OptionalState) (0) | 2024.04.26 |