Skip to content

Commit

Permalink
feat: Basic Observable (#9)
Browse files Browse the repository at this point in the history
* feat: Support Perceptible

* chore: Tweak observation state

---------

Co-authored-by: danthorpe <[email protected]>
  • Loading branch information
danthorpe and danthorpe authored Apr 22, 2024
1 parent 7e26371 commit d71a35b
Show file tree
Hide file tree
Showing 13 changed files with 115 additions and 78 deletions.
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

0 comments on commit d71a35b

Please sign in to comment.