From e71373c576cb1bfefce506cd3827ce132e59f6d6 Mon Sep 17 00:00:00 2001 From: danthorpe Date: Thu, 18 Apr 2024 08:25:45 +0100 Subject: [PATCH 1/2] feat: Support Perceptible --- .../ComposableLoadable/LoadableState.swift | 52 +++++++++++++++---- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/Sources/ComposableLoadable/LoadableState.swift b/Sources/ComposableLoadable/LoadableState.swift index 1447010ff..a81ebe4b3 100644 --- a/Sources/ComposableLoadable/LoadableState.swift +++ b/Sources/ComposableLoadable/LoadableState.swift @@ -14,7 +14,7 @@ public struct LoadedFailure { @dynamicMemberLookup @propertyWrapper -public struct LoadableState { +public struct LoadableState: Perceptible { public static var pending: Self { .init(current: .pending) @@ -59,6 +59,23 @@ public struct LoadableState { internal var previous: State? + private let _$perceptionRegistrar = Perception.PerceptionRegistrar() + + internal nonisolated func access( + keyPath: KeyPath, + file: StaticString = #file, + line: UInt = #line + ) { + _$perceptionRegistrar.access(self, keyPath: keyPath, file: file, line: line) + } + + internal nonisolated func withMutation( + keyPath: KeyPath, + _ mutation: () throws -> MutationResult + ) rethrows -> MutationResult { + try _$perceptionRegistrar.withMutation(of: self, keyPath: keyPath, mutation) + } + internal init(current: State, previous: State? = nil) { self.current = current self.previous = previous @@ -130,6 +147,7 @@ public struct LoadableState { package var loadedValue: LoadedValue? { get { + access(keyPath: \.current) switch (current, previous) { case (.success(let value), _), (_, .success(let value)): return value @@ -138,16 +156,19 @@ public struct LoadableState { } } set { - guard let newValue else { - current = .pending - return + withMutation(keyPath: \.current) { + guard let newValue else { + current = .pending + return + } + current = .success(newValue) } - current = .success(newValue) } } package var loadedFailure: LoadedFailure? { get { + access(keyPath: \.current) switch (current, previous) { case (.failure(let value), _), (_, .failure(let value)): return value @@ -156,11 +177,13 @@ public struct LoadableState { } } set { - guard let newValue else { - current = .pending - return + withMutation(keyPath: \.current) { + guard let newValue else { + current = .pending + return + } + current = .failure(newValue) } - current = .failure(newValue) } } @@ -170,8 +193,15 @@ public struct LoadableState { } public var projectedValue: Self { - get { self } - set { self = newValue } + get { + access(keyPath: \.self) + return self + } + set { + withMutation(keyPath: \.self) { + self = newValue + } + } } public internal(set) var wrappedValue: Value? { From 24106680a856195a732dc557a7fd13c23c160f65 Mon Sep 17 00:00:00 2001 From: danthorpe Date: Mon, 22 Apr 2024 07:43:44 +0100 Subject: [PATCH 2/2] chore: Tweak observation state --- Package.swift | 7 -- Sources/ComposableLoadable/Loadable.swift | 8 -- .../ComposableLoadable/LoadableState.swift | 49 ++------- .../ComposableLoadable/LoadingAction.swift | 1 - .../OpenExistential.swift | 4 +- Sources/ComposableLoadable/Typealiases.swift | 36 +++++-- .../Views/FailureView.swift | 16 +-- .../Views/LoadableView.swift | 100 +++++++++++------- .../Views/OnAppearView.swift | 8 ++ .../LoadableActionTests.swift | 1 - .../LoadableReducerTests.swift | 1 - .../LoadableStateTests.swift | 1 - .../TestFeatureClient.swift | 1 - 13 files changed, 120 insertions(+), 113 deletions(-) rename Sources/{Utilities => ComposableLoadable}/OpenExistential.swift (78%) create mode 100644 Sources/ComposableLoadable/Views/OnAppearView.swift diff --git a/Package.swift b/Package.swift index a3faa3340..49a404e60 100644 --- a/Package.swift +++ b/Package.swift @@ -39,9 +39,6 @@ let 📦 = Module.builder( ComposableLoadable <+ 📦 { $0.createProduct = .library - $0.dependsOn = [ - Utilities - ] $0.with += [ .composableArchitecture ] @@ -49,10 +46,6 @@ ComposableLoadable .swiftTesting ] } -Utilities - <+ 📦 { - $0.createUnitTests = false - } /// ⚙️ Swift Settings /// ------------------------------------------------------------ diff --git a/Sources/ComposableLoadable/Loadable.swift b/Sources/ComposableLoadable/Loadable.swift index 048718a44..4d1be1b7a 100644 --- a/Sources/ComposableLoadable/Loadable.swift +++ b/Sources/ComposableLoadable/Loadable.swift @@ -20,11 +20,3 @@ extension LoadableState where Value: Loadable { self.init(current: .pending) } } - -public typealias LoadableStateOf = LoadableState< - R.State.Request, R.State -> where R.State: Loadable - -public typealias LoadableActionOf = LoadingAction< - R.State.Request, R.State, R.Action -> where R.State: Loadable diff --git a/Sources/ComposableLoadable/LoadableState.swift b/Sources/ComposableLoadable/LoadableState.swift index a81ebe4b3..cb9feab4d 100644 --- a/Sources/ComposableLoadable/LoadableState.swift +++ b/Sources/ComposableLoadable/LoadableState.swift @@ -1,6 +1,5 @@ import ComposableArchitecture import Foundation -import Utilities public struct LoadedValue { package internal(set) var request: Request @@ -14,7 +13,7 @@ public struct LoadedFailure { @dynamicMemberLookup @propertyWrapper -public struct LoadableState: Perceptible { +public struct LoadableState { public static var pending: Self { .init(current: .pending) @@ -59,23 +58,6 @@ public struct LoadableState: Perceptible { internal var previous: State? - private let _$perceptionRegistrar = Perception.PerceptionRegistrar() - - internal nonisolated func access( - keyPath: KeyPath, - file: StaticString = #file, - line: UInt = #line - ) { - _$perceptionRegistrar.access(self, keyPath: keyPath, file: file, line: line) - } - - internal nonisolated func withMutation( - keyPath: KeyPath, - _ mutation: () throws -> MutationResult - ) rethrows -> MutationResult { - try _$perceptionRegistrar.withMutation(of: self, keyPath: keyPath, mutation) - } - internal init(current: State, previous: State? = nil) { self.current = current self.previous = previous @@ -147,7 +129,6 @@ public struct LoadableState: Perceptible { package var loadedValue: LoadedValue? { get { - access(keyPath: \.current) switch (current, previous) { case (.success(let value), _), (_, .success(let value)): return value @@ -156,19 +137,16 @@ public struct LoadableState: Perceptible { } } set { - withMutation(keyPath: \.current) { - guard let newValue else { - current = .pending - return - } - current = .success(newValue) + guard let newValue else { + current = .pending + return } + current = .success(newValue) } } package var loadedFailure: LoadedFailure? { get { - access(keyPath: \.current) switch (current, previous) { case (.failure(let value), _), (_, .failure(let value)): return value @@ -177,13 +155,11 @@ public struct LoadableState: Perceptible { } } set { - withMutation(keyPath: \.current) { - guard let newValue else { - current = .pending - return - } - current = .failure(newValue) + guard let newValue else { + current = .pending + return } + current = .failure(newValue) } } @@ -194,13 +170,10 @@ public struct LoadableState: Perceptible { public var projectedValue: Self { get { - access(keyPath: \.self) - return self + self } set { - withMutation(keyPath: \.self) { - self = newValue - } + self = newValue } } diff --git a/Sources/ComposableLoadable/LoadingAction.swift b/Sources/ComposableLoadable/LoadingAction.swift index d225660fd..b747cc1df 100644 --- a/Sources/ComposableLoadable/LoadingAction.swift +++ b/Sources/ComposableLoadable/LoadingAction.swift @@ -1,6 +1,5 @@ import ComposableArchitecture import Foundation -import Utilities @CasePathable public enum LoadingAction { diff --git a/Sources/Utilities/OpenExistential.swift b/Sources/ComposableLoadable/OpenExistential.swift similarity index 78% rename from Sources/Utilities/OpenExistential.swift rename to Sources/ComposableLoadable/OpenExistential.swift index b4011d63a..9ee49b2c9 100644 --- a/Sources/Utilities/OpenExistential.swift +++ b/Sources/ComposableLoadable/OpenExistential.swift @@ -2,7 +2,7 @@ // MARK: Equatable -package func _isEqual(_ lhs: Any, _ rhs: Any) -> Bool { +func _isEqual(_ lhs: Any, _ rhs: Any) -> Bool { (lhs as? any Equatable)?.isEqual(other: rhs) ?? false } @@ -14,7 +14,7 @@ extension Equatable { // MARK: Identifiable -package func _identifiableID(_ value: Any) -> AnyHashable? { +func _identifiableID(_ value: Any) -> AnyHashable? { func open(_ value: some Identifiable) -> AnyHashable { value.id } diff --git a/Sources/ComposableLoadable/Typealiases.swift b/Sources/ComposableLoadable/Typealiases.swift index 80334859f..20aaabcfe 100644 --- a/Sources/ComposableLoadable/Typealiases.swift +++ b/Sources/ComposableLoadable/Typealiases.swift @@ -6,18 +6,42 @@ public typealias LoadableStateWith = LoadableState< Request, R.State > +public typealias LoadableStateOf = LoadableStateWith< + R.State.Request, R +> where R.State: Loadable + public typealias LoadingActionWith = LoadingAction< Request, R.State, R.Action > -public typealias LoadableStoreWith = Store< - LoadableStateWith, LoadingActionWith +public typealias LoadingActionOf = LoadingActionWith< + R.State.Request, R +> where R.State: Loadable + +public typealias LoadableStore = Store< + LoadableState, LoadingAction +> + +public typealias LoadableStoreWith = LoadableStore< + Request, R.State, R.Action +> + +public typealias LoadableStoreOf = LoadableStoreWith< + R.State.Request, R +> where R.State: Loadable + +public typealias LoadedValueStore = Store< + LoadedValue, LoadingAction +> + +public typealias LoadedValueStoreWith = LoadedValueStore< + Request, R.State, R.Action > -public typealias LoadedValueStoreWith = Store< - LoadedValue, LoadingActionWith +public typealias LoadedFailureStore = Store< + LoadedFailure, LoadingAction > -public typealias LoadedFailureStoreWith = Store< - LoadedFailure, LoadingActionWith +public typealias LoadedFailureStoreWith = LoadedFailureStore< + Request, Failure, R.State, R.Action > diff --git a/Sources/ComposableLoadable/Views/FailureView.swift b/Sources/ComposableLoadable/Views/FailureView.swift index 9b6da53b0..086fbaf56 100644 --- a/Sources/ComposableLoadable/Views/FailureView.swift +++ b/Sources/ComposableLoadable/Views/FailureView.swift @@ -1,16 +1,16 @@ import ComposableArchitecture import SwiftUI -public struct FailureView { - public typealias FailureStore = Store< - LoadedFailure, LoadingAction - > - public typealias ContentBuilder = (Failure, Request) -> Content +public struct FailureView { + typealias ContentBuilder = (any Error, Request) -> Content - let store: FailureStore + let store: LoadedFailureStore let content: ContentBuilder - public init(store: FailureStore, @ViewBuilder content: @escaping ContentBuilder) { + init( + store: LoadedFailureStore, + @ViewBuilder content: @escaping ContentBuilder + ) { self.store = store self.content = content } @@ -18,7 +18,7 @@ public struct FailureView extension FailureView: View { public var body: some View { - WithViewStore(store, observe: { $0 }) { viewStore in + WithViewStore(store, observe: { $0 }, removeDuplicates: _isEqual) { viewStore in content(viewStore.error, viewStore.request) } } diff --git a/Sources/ComposableLoadable/Views/LoadableView.swift b/Sources/ComposableLoadable/Views/LoadableView.swift index c8262f26f..e63315bae 100644 --- a/Sources/ComposableLoadable/Views/LoadableView.swift +++ b/Sources/ComposableLoadable/Views/LoadableView.swift @@ -1,33 +1,33 @@ import ComposableArchitecture import SwiftUI -import Utilities public struct LoadableView< Request, - Feature: Reducer, - SuccessView: View, - FailureView: View, - LoadingView: View, - PendingView: View -> { + State, + Action, + Success: View, + Failure: View, + Loading: View, + Pending: View +>: View { - public typealias SuccessContentBuilder = (LoadedValueStoreWith) -> SuccessView - public typealias FailureContentBuilder = (LoadedFailureStoreWith) -> - FailureView - public typealias LoadingContentBuilder = (Request) -> FailureView + public typealias SuccessViewBuilder = @MainActor (LoadedValueStore) -> Success + public typealias FailureViewBuilder = @MainActor (LoadedFailureStore) -> + Failure + public typealias LoadingViewBuilder = @MainActor (Request) -> Loading - let store: LoadableStoreWith - let successView: SuccessContentBuilder - let failureView: FailureContentBuilder - let loadingView: LoadingContentBuilder - let pendingView: PendingView + let store: LoadableStore + let successView: SuccessViewBuilder + let failureView: FailureViewBuilder + let loadingView: LoadingViewBuilder + let pendingView: Pending public init( - _ store: LoadableStoreWith, - @ViewBuilder pending: () -> PendingView, - @ViewBuilder loading: @escaping LoadingContentBuilder, - @ViewBuilder failure: @escaping FailureContentBuilder, - @ViewBuilder success: @escaping SuccessContentBuilder + _ store: LoadableStore, + @ViewBuilder success: @escaping SuccessViewBuilder, + @ViewBuilder failure: @escaping FailureViewBuilder, + @ViewBuilder loading: @escaping LoadingViewBuilder, + @ViewBuilder pending: () -> Pending ) { self.store = store self.successView = success @@ -36,20 +36,45 @@ public struct LoadableView< self.pendingView = pending() } - public init( - _ store: LoadableStoreWith, - @ViewBuilder pending: () -> PendingView, - @ViewBuilder loading: @escaping LoadingContentBuilder, - @ViewBuilder failure: @escaping FailureContentBuilder, - @ViewBuilder feature: @escaping (StoreOf) -> SuccessView - ) { - self.init(store, pending: pending, loading: loading, failure: failure) { - feature( - $0.scope( - state: \.value, - action: \.loaded - ) - ) + public init( + _ store: LoadableStore, + @ViewBuilder feature: @escaping (Store) -> SuccessView, + @ViewBuilder onError: @escaping (any Error, Request) -> ErrorView, + @ViewBuilder onActive: @escaping (Request) -> Loading, + onAppear: @escaping () -> Void = {} + ) + where + Pending == OnAppearView, + Failure == FailureView, + Success == WithPerceptionTracking + { + self.init(store) { loadedStore in + WithPerceptionTracking { + feature(loadedStore.scope(state: \.value, action: \.loaded)) + } + } failure: { + FailureView(store: $0, content: onError) + } loading: { + onActive($0) + } pending: { + OnAppearView(block: onAppear) + } + } + + public init( + loadOnAppear store: LoadableStore, + @ViewBuilder feature: @escaping (Store) -> SuccessView, + @ViewBuilder onError: @escaping (any Error, Request) -> ErrorView, + @ViewBuilder onActive: @escaping (Request) -> Loading + ) + where + Request == EmptyLoadRequest, + Pending == OnAppearView, + Failure == FailureView, + Success == WithPerceptionTracking + { + self.init(store, feature: feature, onError: onError, onActive: onActive) { + store.send(.load) } } @@ -74,16 +99,13 @@ public struct LoadableView< let isNotRefreshing: Bool let isActiveRequest: Request? - init(state: LoadableState) { + init(state: LoadableState) { self.isPending = state.isPending self.isLoaded = state.isSuccess || state.isFailure self.isNotRefreshing = false == state.isRefreshing self.isActiveRequest = state.isActive ? state.request : nil } } -} - -extension LoadableView: View { public var body: some View { WithViewStore(store, observe: ViewState.init) { viewStore in diff --git a/Sources/ComposableLoadable/Views/OnAppearView.swift b/Sources/ComposableLoadable/Views/OnAppearView.swift new file mode 100644 index 000000000..c8d9572dc --- /dev/null +++ b/Sources/ComposableLoadable/Views/OnAppearView.swift @@ -0,0 +1,8 @@ +import SwiftUI + +public struct OnAppearView: View { + let block: () -> Void + public var body: some View { + Color.clear.onAppear(perform: block) + } +} diff --git a/Tests/ComposableLoadableTests/LoadableActionTests.swift b/Tests/ComposableLoadableTests/LoadableActionTests.swift index eb1fad84d..80f7c77ae 100644 --- a/Tests/ComposableLoadableTests/LoadableActionTests.swift +++ b/Tests/ComposableLoadableTests/LoadableActionTests.swift @@ -1,6 +1,5 @@ import ComposableArchitecture import Testing -import Utilities @testable import ComposableLoadable diff --git a/Tests/ComposableLoadableTests/LoadableReducerTests.swift b/Tests/ComposableLoadableTests/LoadableReducerTests.swift index e21f61bc4..ff9f8a909 100644 --- a/Tests/ComposableLoadableTests/LoadableReducerTests.swift +++ b/Tests/ComposableLoadableTests/LoadableReducerTests.swift @@ -1,6 +1,5 @@ import ComposableArchitecture import Testing -import Utilities @testable import ComposableLoadable diff --git a/Tests/ComposableLoadableTests/LoadableStateTests.swift b/Tests/ComposableLoadableTests/LoadableStateTests.swift index 47c8c326f..24bac6d04 100644 --- a/Tests/ComposableLoadableTests/LoadableStateTests.swift +++ b/Tests/ComposableLoadableTests/LoadableStateTests.swift @@ -1,5 +1,4 @@ import Testing -import Utilities @testable import ComposableLoadable diff --git a/Tests/ComposableLoadableTests/TestFeatureClient.swift b/Tests/ComposableLoadableTests/TestFeatureClient.swift index d799bb772..03dd78fe2 100644 --- a/Tests/ComposableLoadableTests/TestFeatureClient.swift +++ b/Tests/ComposableLoadableTests/TestFeatureClient.swift @@ -1,6 +1,5 @@ import ComposableArchitecture import Testing -import Utilities @testable import ComposableLoadable