2024년 02월에 첫 commit을 찍은 나의 TCA 프로젝트는 당시 최신 버전이었던 1.8.2
버전에 의존하고 있다. 현 시점 pointfreeco는 1.10.2
를 24년 5월 12일에 릴리즈하였다. 소스 프리징은 몇 주 앞둔 시점이라 무리하게 버전업하기 보다는 1.9, 1.10 버전에 어떤 변화가 생겼는지 Migration Guide를 기반으로 살펴보고 내 프로젝트에 반영하면 좋을 것을 추려보려한다.
https://github.com/pointfreeco/swift-composable-architecture/releases/tag/1.10.4
그들의 표현을 빌리자면,
"The Composable Architecture is under constant development, and we are always looking for ways to
simplify the library, and make it more powerful."
정말 더욱 편리해지고 강력해진 라이브러리이다.
WritableKeyPath
에 의존하고 있다. 문제는 struct 타입은 keypath를 기본 지원하지만 enum 타입은 똑같은 value type임에도 저장 프로퍼티를 가지고 있지 않으므로 keypath를 지원하지 않는다는 점이다. TCA에서 @Reducer 자체는 struct이지만 state든 action이든 enum 타입을 사용하고 있어서 부득이하게 이러한 casepathable이 필요했을 것으로 생각된다.-store.send(.path(.element(id: 0, action: .destination(.presented(.record(.startButtonTapped))))))
+store.send(\.path[id: 0].destination.record.startButtonTapped)
@DependencyClient
struct DownloadClient {
var download: @Sendable (_ url: URL) -> AsyncThrowingStream<Event, Error> = { _ in .finished() }
@CasePathable
enum Event: Equatable {
case response(Data)
case updateProgress(Double)
}
}
extension DownloadClient: DependencyKey {
static let liveValue = Self(
download: { url in
.init { continuation in
Task {
do {
let (bytes, response) = try await URLSession.shared.bytes(from: url)
var data = Data()
var progress = 0
for try await byte in bytes {
data.append(byte)
let newProgress = Int(
Double(data.count) / Double(response.expectedContentLength) * 100)
if newProgress != progress {
progress = newProgress
continuation.yield(.updateProgress(Double(progress) / 100))
}
}
continuation.yield(.response(data))
continuation.finish()
} catch {
continuation.finish(throwing: error)
}
}
}
}
)
static let testValue = Self()
}
self.downloadClient.download
를 사용할 수 있는 이유이다.@Reducer
struct DownloadComponent {
struct State: Equatable {
@PresentationState var alert: AlertState<Action.Alert>?
let id: AnyHashable
var mode: Mode
let url: URL
}
// action 생략
@Dependency(\.downloadClient) var downloadClient
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .buttonTapped:
switch state.mode {
// ... case 생략
case .notDownloaded:
state.mode = .startingToDownload
return .run { [url = state.url] send in
for try await event in self.downloadClient.download(url: url) {
await send(.downloadClient(.success(event)), animation: .default)
}
} catch: { error, send in
await send(.downloadClient(.failure(error)), animation: .default)
}
.cancellable(id: state.id)
}
extension DependencyValues {
var downloadClient: DownloadClient {
get { self[DownloadClient.self] }
set { self[DownloadClient.self] = newValue }
}
}
-@Dependency(\.apiClient) var apiClient
+@Dependency(APIClient.self) var apiClient
@ObservableState
struct State {
@Shared var signUpData: SignUpData
// ...
}
class testView: View {
@AppStorage("savedProp") var myName: String = ""
@Bindable var store: StoreOf<PersonalStore>
var body: some View {
WithPerceptionTracking {
SomeView()
.onChange(of: myName) { newValue in
store.send(.write(newValue))
// OR
@Reducer
struct PersonalStore {
@ObservableState
struct State: Equatable {
var name: String {
get {
if let name = UserDefaults.standard.object(forKey: nameKey) as? String {
return name
}
return ""
set {
UserDefaults.standard.set(newValue, forKey: nameKey)
@Reducer
struct PersonalStore {
@ObservableState
struct State {
@Shared(.appStorage(nameKey)) var name = "defaultName"
@ObservableState
struct State: Equatable {
@Presents var destination: Destination.State?
@Shared(.fileStorage(.syncUps)) var syncUps: IdentifiedArrayOf<SyncUp> = []
}
pointfreeco가 소개하는 Shared
https://www.pointfree.co/blog/posts/134-sharing-state-in-the-composable-architecture
간략한 예제 코드
https://github.com/pointfreeco/swift-composable-architecture/blob/7bd346042b3168894b538f49803d25497885c81c/Examples/SyncUps/SyncUps/SyncUpsList.swift#L18