Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic Observable #9

Merged
merged 2 commits into from
Apr 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,13 @@ let 📦 = Module.builder(
ComposableLoadable
<+ 📦 {
$0.createProduct = .library
$0.dependsOn = [
Utilities
]
$0.with += [
.composableArchitecture
]
$0.unitTestsWith += [
.swiftTesting
]
}
Utilities
<+ 📦 {
$0.createUnitTests = false
}

/// ⚙️ Swift Settings
/// ------------------------------------------------------------
Expand Down
8 changes: 0 additions & 8 deletions Sources/ComposableLoadable/Loadable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,3 @@ extension LoadableState where Value: Loadable {
self.init(current: .pending)
}
}

public typealias LoadableStateOf<R: Reducer> = LoadableState<
R.State.Request, R.State
> where R.State: Loadable

public typealias LoadableActionOf<R: Reducer> = LoadingAction<
R.State.Request, R.State, R.Action
> where R.State: Loadable
9 changes: 6 additions & 3 deletions Sources/ComposableLoadable/LoadableState.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import ComposableArchitecture
import Foundation
import Utilities

public struct LoadedValue<Request, Value> {
package internal(set) var request: Request
Expand Down Expand Up @@ -170,8 +169,12 @@ public struct LoadableState<Request, Value> {
}

public var projectedValue: Self {
get { self }
set { self = newValue }
get {
self
}
set {
self = newValue
}
}

public internal(set) var wrappedValue: Value? {
Expand Down
1 change: 0 additions & 1 deletion Sources/ComposableLoadable/LoadingAction.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import ComposableArchitecture
import Foundation
import Utilities

@CasePathable
public enum LoadingAction<Request, Value, Action> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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
}
Expand Down
36 changes: 30 additions & 6 deletions Sources/ComposableLoadable/Typealiases.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,42 @@ public typealias LoadableStateWith<Request, R: Reducer> = LoadableState<
Request, R.State
>

public typealias LoadableStateOf<R: Reducer> = LoadableStateWith<
R.State.Request, R
> where R.State: Loadable

public typealias LoadingActionWith<Request, R: Reducer> = LoadingAction<
Request, R.State, R.Action
>

public typealias LoadableStoreWith<Request, R: Reducer> = Store<
LoadableStateWith<Request, R>, LoadingActionWith<Request, R>
public typealias LoadingActionOf<R: Reducer> = LoadingActionWith<
R.State.Request, R
> where R.State: Loadable

public typealias LoadableStore<Request, State, Action> = Store<
LoadableState<Request, State>, LoadingAction<Request, State, Action>
>

public typealias LoadableStoreWith<Request, R: Reducer> = LoadableStore<
Request, R.State, R.Action
>

public typealias LoadableStoreOf<R: Reducer> = LoadableStoreWith<
R.State.Request, R
> where R.State: Loadable

public typealias LoadedValueStore<Request, State, Action> = Store<
LoadedValue<Request, State>, LoadingAction<Request, State, Action>
>

public typealias LoadedValueStoreWith<Request, R: Reducer> = LoadedValueStore<
Request, R.State, R.Action
>

public typealias LoadedValueStoreWith<Request, R: Reducer> = Store<
LoadedValue<Request, R.State>, LoadingActionWith<Request, R>
public typealias LoadedFailureStore<Request, Failure: Error, State, Action> = Store<
LoadedFailure<Request, Failure>, LoadingAction<Request, State, Action>
>

public typealias LoadedFailureStoreWith<Request, Failure: Error, R: Reducer> = Store<
LoadedFailure<Request, Failure>, LoadingActionWith<Request, R>
public typealias LoadedFailureStoreWith<Request, Failure: Error, R: Reducer> = LoadedFailureStore<
Request, Failure, R.State, R.Action
>
16 changes: 8 additions & 8 deletions Sources/ComposableLoadable/Views/FailureView.swift
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import ComposableArchitecture
import SwiftUI

public struct FailureView<Request, Failure: Error, State, Action, Content: View> {
public typealias FailureStore = Store<
LoadedFailure<Request, Failure>, LoadingAction<Request, State, Action>
>
public typealias ContentBuilder = (Failure, Request) -> Content
public struct FailureView<Request, State, Action, Content: View> {
typealias ContentBuilder = (any Error, Request) -> Content

let store: FailureStore
let store: LoadedFailureStore<Request, Error, State, Action>
let content: ContentBuilder

public init(store: FailureStore, @ViewBuilder content: @escaping ContentBuilder) {
init(
store: LoadedFailureStore<Request, Error, State, Action>,
@ViewBuilder content: @escaping ContentBuilder
) {
self.store = store
self.content = content
}
}

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)
}
}
Expand Down
100 changes: 61 additions & 39 deletions Sources/ComposableLoadable/Views/LoadableView.swift
Original file line number Diff line number Diff line change
@@ -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<Request, Feature>) -> SuccessView
public typealias FailureContentBuilder = (LoadedFailureStoreWith<Request, Error, Feature>) ->
FailureView
public typealias LoadingContentBuilder = (Request) -> FailureView
public typealias SuccessViewBuilder = @MainActor (LoadedValueStore<Request, State, Action>) -> Success
public typealias FailureViewBuilder = @MainActor (LoadedFailureStore<Request, Error, State, Action>) ->
Failure
public typealias LoadingViewBuilder = @MainActor (Request) -> Loading

let store: LoadableStoreWith<Request, Feature>
let successView: SuccessContentBuilder
let failureView: FailureContentBuilder
let loadingView: LoadingContentBuilder
let pendingView: PendingView
let store: LoadableStore<Request, State, Action>
let successView: SuccessViewBuilder
let failureView: FailureViewBuilder
let loadingView: LoadingViewBuilder
let pendingView: Pending

public init(
_ store: LoadableStoreWith<Request, Feature>,
@ViewBuilder pending: () -> PendingView,
@ViewBuilder loading: @escaping LoadingContentBuilder,
@ViewBuilder failure: @escaping FailureContentBuilder,
@ViewBuilder success: @escaping SuccessContentBuilder
_ store: LoadableStore<Request, State, Action>,
@ViewBuilder success: @escaping SuccessViewBuilder,
@ViewBuilder failure: @escaping FailureViewBuilder,
@ViewBuilder loading: @escaping LoadingViewBuilder,
@ViewBuilder pending: () -> Pending
) {
self.store = store
self.successView = success
Expand All @@ -36,20 +36,45 @@ public struct LoadableView<
self.pendingView = pending()
}

public init(
_ store: LoadableStoreWith<Request, Feature>,
@ViewBuilder pending: () -> PendingView,
@ViewBuilder loading: @escaping LoadingContentBuilder,
@ViewBuilder failure: @escaping FailureContentBuilder,
@ViewBuilder feature: @escaping (StoreOf<Feature>) -> SuccessView
) {
self.init(store, pending: pending, loading: loading, failure: failure) {
feature(
$0.scope(
state: \.value,
action: \.loaded
)
)
public init<SuccessView: View, ErrorView: View>(
_ store: LoadableStore<Request, State, Action>,
@ViewBuilder feature: @escaping (Store<State, Action>) -> SuccessView,
@ViewBuilder onError: @escaping (any Error, Request) -> ErrorView,
@ViewBuilder onActive: @escaping (Request) -> Loading,
onAppear: @escaping () -> Void = {}
)
where
Pending == OnAppearView,
Failure == FailureView<Request, State, Action, ErrorView>,
Success == WithPerceptionTracking<SuccessView>
{
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<SuccessView: View, ErrorView: View>(
loadOnAppear store: LoadableStore<Request, State, Action>,
@ViewBuilder feature: @escaping (Store<State, Action>) -> SuccessView,
@ViewBuilder onError: @escaping (any Error, Request) -> ErrorView,
@ViewBuilder onActive: @escaping (Request) -> Loading
)
where
Request == EmptyLoadRequest,
Pending == OnAppearView,
Failure == FailureView<Request, State, Action, ErrorView>,
Success == WithPerceptionTracking<SuccessView>
{
self.init(store, feature: feature, onError: onError, onActive: onActive) {
store.send(.load)
}
}

Expand All @@ -74,16 +99,13 @@ public struct LoadableView<
let isNotRefreshing: Bool
let isActiveRequest: Request?

init(state: LoadableState<Request, Feature.State>) {
init(state: LoadableState<Request, State>) {
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
Expand Down
8 changes: 8 additions & 0 deletions Sources/ComposableLoadable/Views/OnAppearView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import SwiftUI

public struct OnAppearView: View {
let block: () -> Void
public var body: some View {
Color.clear.onAppear(perform: block)
}
}
1 change: 0 additions & 1 deletion Tests/ComposableLoadableTests/LoadableActionTests.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import ComposableArchitecture
import Testing
import Utilities

@testable import ComposableLoadable

Expand Down
1 change: 0 additions & 1 deletion Tests/ComposableLoadableTests/LoadableReducerTests.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import ComposableArchitecture
import Testing
import Utilities

@testable import ComposableLoadable

Expand Down
1 change: 0 additions & 1 deletion Tests/ComposableLoadableTests/LoadableStateTests.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Testing
import Utilities

@testable import ComposableLoadable

Expand Down
1 change: 0 additions & 1 deletion Tests/ComposableLoadableTests/TestFeatureClient.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import ComposableArchitecture
import Testing
import Utilities

@testable import ComposableLoadable

Expand Down