apple/TCA

[TCA] 공부기록 #1 (ReducerProtocol)

lgvv 2023. 1. 16. 18:31

#### TCA 공부시작 

 

 

(첫 포스팅 날짜) 2023. 1. 16. 18:31
(업데이트) 2023. 10. 08. 01:08 
 - 업데이트 사유: TCA 1.0.0 출시로 인한 공부 계획 업데이트. 해당 포스팅은 현재 크게 의미가 없는 상태
 - 환경
   - Xcode 15.0
   - TCA 1.0.0
 - TCA 공부는 아래 링크에서 확인 가능
https://rldd.tistory.com/category/apple/%F0%9F%A6%A5%20TCA

 

'apple/🦥 TCA' 카테고리의 글 목록

 

rldd.tistory.com

 

 

 

 

https://github.com/pointfreeco/swift-composable-architecture

 

GitHub - pointfreeco/swift-composable-architecture: A library for building applications in a consistent and understandable way,

A library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind. - GitHub - pointfreeco/swift-composable-architecture: A library for bu...

github.com

 

 

#### 환경

 - XCode: 14.0

 - swift-composable-architecture ~> main

 

 

#### 어떻게 학습할 것인가?

 - TCA의 Tic-Tac-Toc Example을 분석해보면 토이 프로젝트에 적용해보기.

 

### 목표

 - TCA의 많은 부분을 사용해보기

 

### 작업 후 개인적인 정리

 - IfLetStore: nil타입에서 사용 가능하여 nil 데이터 처리와 SwiftUI에 View를 더 깔끔하게처리 가능해 보임.

 

 - Scope: onAppear가 중복 호출되는 이슈가 있었는데, Scope를 통해 범위를 지정하고 나눌 수 있음.

 

 - View(Action, State), Core(Action, State)는 독립적으로 존재하는 것이 좋다고 판단. 

 : ReactorKit을 생각하면 동일한게 아닐까를 고민했으나, 둘이 나뉨으로써 Core의 Action이 더 명확하고 깔끔해진 기분이 들었음.

 : 이 방식이 정말 좋다고 느끼는데, 코드 관리가 더 잘 되는 기분이다!

 

 - TCA의 ViewModel 대신에 Core라는 네이밍으로 작성하기.

 

 - Effect를 처리할 때, Core의 Action으로 분리했기 때문에 Effect.Action을 통해 더 명확하게 기능 역할을 분리할 수 있음

 

 - @Dependency 사용

 : DependencyKey를 상속받아서 구현해야 하며, DependencyValue를 통해서 작업할 수 있음.

  -> 기존 프로젝트에 이식하면서 발견한 문제점 및 고민(DIP가 이래서 필요한거구나를 느낌)

 : SDK에서 Network와 로컬 DB를 들고 있음.

 : @Dependency를 사용할 경우 get-only으로 나타남. (확실하게 get-only인지는 모르겠음 set으로 하는 방법도 있을 것 같은데, 이건 조사기 필요합니다.)

 

 - pullback을 더 간결하게 처리하는 방법

 : SwiftUI를 사용하면서 뷰의 계층이 깊어지면 나누곤하는데, 이벤트 처리가 아직 미숙해서 하나의 파일에 때려박은 느낌이 없지않아 있다. 이 부분은 어떻게 개선할지 꼭 고민해보기

 

@Depedency(\.sdkService) var sdkService

// ✅ 네트워크 관련 비동기 작업에서는 문제가 없이 사용가능
sdkService.fetchAllItem
	.catchToEffect(Action.fetchAllItemSuccess)
    // 후략
    
    
// 🚨 로컬 DB의 데이터를 저장해야 한다면?
sdkService.location = location // get-only error 발생

 

 

### 다음 목표

 - TCACoordinator를 적용하여 화면 3개이상의 화면전환을 구현해보기.

 

#### 시작하기 전 유의사항

 

https://github.com/pointfreeco/swift-composable-architecture/discussions/1477

 

Road to 1.0 · Discussion #1477 · pointfreeco/swift-composable-architecture

It’s been almost 2 and a half years since we first released the Composable Architecture, so we’ve been getting more and more questions about its 0.x.x prerelease versioning, what’s holding up a 1.0...

github.com

 

# Removing the old Reducer struct

✅  ReducerProtocol(Reducer) 사용해라. 

  - 1.0 릴리즈 다가오면 AnyReducer fully deprecated(완전 중단) 할거라고 함.

  - 현재는 Deprecated 되었다는 노란색 문구가 나타남.

 

✅ 변경 내용

  - typealias Reducer = AnyReducer는 삭제 예정 (0.43.0) 버전에서 전역으로 Reducer를 사용하고 있음.

  - ReducerProtocol의 이름을 Reducer로 변경

  - Introducing a hard-deprecated typealias ReducerProtocol = Reducer

   : 세번째는 정확하게 의미는 모르겠는데, 아래 영어를 참고하자면 Reducer로 프로토콜 이름 적용하란 의미인듯

 

If you have already updated your code base to 0.43.0, then none of these changes are breaking. You will just have to rename occurrences of ReducerProtocol to Reducer, which Xcode should be able to help with.

 

# Removing many Combine-specific features of Effect

Concurrency 릴리즈하면서 (.run, .task, .fireAndForgot)을 사용하고 3가지 진입점에 맞추어서 유형을 단순화.

따라서 불필요한 레거시를 제거하고자 함.

 

Effect의 경우에는 Combine publisher로 사용하도록 설계되었기 때문에, 더이상 필요하지 않은 몇몇 부분에서 제거하기로 함.

예를 들여서 Publisher Protocol에서 Failure로 했지만 reducer안에서 Never로 잠겨있는 등.

 

그러니까 Failure는 더이상 의믜가 없으므로, Effect<Action, Failure> -> Effect<Action>으로 단순화하고자 함.

 

 

 -  Effect 제거할거야.

 -  EffectTask -> Effect로 이름 바꿀거야

 - 기존 EffectTask는 계속 작동하도록 지원할거지만 Xcode에서 Deprecated된거 알려주어서 너가 변경하도록 도울거야.

 

 

 

#### UI 결과물

결과물 UI.gif

 

 

#### 코드

 

//  LocationSetting.swift

//
//  LocationSetting.swift
//  UJeongApp
//
//  Created by Hamlit Jason on 2023/01/03.


import SwiftUI
import ComposableArchitecture

// MARK: - LocationSettingView

public struct LocationSettingView: View {
    typealias Core = LocationSettingCore
    
    private let store: StoreOf<Core>
    @Environment(\.dismiss) private var dismiss
    
    struct ViewState: Equatable {
        let allLocationSection = LocationSection.allSection()
        public var selectedLocation: String
        
        init(state: Core.State) {
            self.selectedLocation = state.selectedLocation
        }
    }
    
    enum ViewAction {
        case itemSelected(location: String)
        case onAppear
    }
    
    init(store: StoreOf<Core>) {
        self.store = store
    }
    
    let columns = [GridItem(.flexible()),
                   GridItem(.flexible()),
                   GridItem(.flexible())]
    
    public var body: some View {
        WithViewStore(
            self.store,
            observe: ViewState.init,
            send: Core.Action.init
        ) { viewStore in
            NavigationView {
                
                ForEach(viewStore.state.allLocationSection) { section in
                    List {
                        Text("내가 선택한 지역:  \(viewStore.selectedLocation)")
                        
                        Section {
                            LazyVGrid(
                                columns: columns,
                                alignment: .center,
                                spacing: .none
                            ) {
                                ForEach(section.location.districts, id: \.self) { location in
                                    
                                    Text(location)
                                        .padding(.horizontal, 5)
                                        .padding(.vertical, 5)
                                        .overlay {
                                            if location == viewStore.state.selectedLocation {
                                                RoundedRectangle(cornerRadius: 10)
                                                    .stroke(Color.black, lineWidth: 3)
                                            }
                                        }
                                        .onTapGesture {
                                            viewStore.send(.itemSelected(location: location))
                                        }
                                }
                            }
                        } header: {
                            Text(section.location.city)
                        }
                    }
                    .listStyle(.sidebar)
                }
                
                
                .onAppear {
                    viewStore.send(.onAppear)
                }
                .navigationBarTitle("지역 선택", displayMode: .inline)
                .toolbar {
                    Button {
                        dismiss()
                    } label: {
                        Image(systemName: "checkmark")
                    }
                }
            }
        }
    }
}

 

//  LocationSettingCore.swift

//
//  LocationSettingCore.swift
//  UJeongApp
//
//  Created by Hamlit Jason on 2023/01/16.
//

import Foundation
import ComposableArchitecture

public struct LocationSettingCore: ReducerProtocol {
    public struct State: Equatable {
        var selectedLocation: String = ""
    }
    
    public enum Action {
        case onAppear
        case itemSelected(location: String)
        case updateAppStorage
        
        init(action: LocationSettingView.ViewAction) {
            switch action {
            case .onAppear:
                self = .onAppear
            case let .itemSelected(location):
                self = .itemSelected(location: location)
            }
        }
    }
    
    // NOTE: - func reduce랑 동일
    public var body: some ReducerProtocol<State, Action> {
        Reduce { state, action in
            switch action {
            case .onAppear:
                state.selectedLocation = sdkService.selectedLocation
                
                return .none
            case let .itemSelected(location):
                state.selectedLocation = location
                
                return EffectTask<Action>(value: .updateAppStorage)
            case .updateAppStorage:
                sdkService.selectedLocation = state.selectedLocation
                
                return .none
            }
        }
    }
    
    private let sdkService = UJeongSDKService()
}

 

// LocationSection.Swift

 

//
//  LocationSection.swift
//  UJeongApp
//
//  Created by Hamlit Jason on 2023/01/16.
//

import Foundation

struct LocationSection: Identifiable, Equatable {
    let id = UUID()
    
    let location: Location
    
    static func allSection() -> [LocationSection] {
        let seoul = LocationSection(location: .init(city: "서울특별시",
                                                    districts: Gu.allCases.map { $0.rawValue }.sorted(by: <)))
        
        return [seoul]
    }
}

 

// Gu.Swift

//
//  Location.swift
//  UJeongApp
//
//  Created by Hamlit Jason on 2023/01/04.
//

import Foundation

/*
 대도시는 구로 분류할 수 있다면 구단위로 분류합니다.
 
 만약 지방 소도시의 경우에는 구단위로 분류하기가 어렵다면 시단위로 분류할 수 있습니다.
 차후 확장성을 늘 생각합시다.
 
 */

public struct Location: Hashable {
    public var city: String // 도시이름
    public var districts: [String] // 지역구
}

public enum Gu: String, CaseIterable {
    case 강서구
    case 양천구
    case 구로구
    case 금천구
    case 관악구
    
    case 영등포구
    case 동작구
    case 서초구
    case 강남구
    case 송파구
    
    case 강동구
    case 용산구
    case 성동구
    case 광진구
    case 동대문구
    
    case 중랑구
    case 노원구
    case 도봉구
    case 강북구
    case 성북구
    
    case 종로구
    case 서대문구
    case 중구
    case 마포구
    case 은평구
}

'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] Tutorial #5 (Multiple presentation destinations)  (0) 2023.09.24