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 1447010ff..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 @@ -170,8 +169,12 @@ public struct LoadableState { } public var projectedValue: Self { - get { self } - set { self = newValue } + get { + self + } + set { + self = newValue + } } public internal(set) var wrappedValue: Value? { 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