Skip to content

Commit db2a55f

Browse files
authored
Expose StorePublisher directly on Store (#2330)
Now that `ViewStore`s have a limited future, we should surface functionality that was `ViewStore`-only to the `Store` instead.
1 parent 2c93195 commit db2a55f

File tree

3 files changed

+53
-44
lines changed

3 files changed

+53
-44
lines changed

Sources/ComposableArchitecture/Documentation.docc/Extensions/UIKit.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ While the Composable Architecture was designed with SwiftUI in mind, it comes wi
1414

1515
### Subscribing to state changes
1616

17+
- ``Store/publisher``
1718
- ``ViewStore/publisher``

Sources/ComposableArchitecture/Store.swift

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,19 @@ public final class Store<State, Action> {
673673
#endif
674674
self.threadCheck(status: .`init`)
675675
}
676+
677+
/// A publisher that emits when state changes.
678+
///
679+
/// This publisher supports dynamic member lookup so that you can pluck out a specific field in
680+
/// the state:
681+
///
682+
/// ```swift
683+
/// store.publisher.alert
684+
/// .sink { ... }
685+
/// ```
686+
public var publisher: StorePublisher<State> {
687+
StorePublisher(store: self, upstream: self.state)
688+
}
676689
}
677690

678691
/// A convenience type alias for referring to a store of a given reducer's domain.
@@ -795,6 +808,44 @@ extension ScopedReducer: AnyScopedReducer {
795808
}
796809
}
797810

811+
/// A publisher of store state.
812+
@dynamicMemberLookup
813+
public struct StorePublisher<State>: Publisher {
814+
public typealias Output = State
815+
public typealias Failure = Never
816+
817+
let store: Any
818+
let upstream: AnyPublisher<State, Never>
819+
820+
init<P: Publisher>(
821+
store: Any,
822+
upstream: P
823+
) where P.Output == Output, P.Failure == Failure {
824+
self.store = store
825+
self.upstream = upstream.eraseToAnyPublisher()
826+
}
827+
828+
public func receive<S: Subscriber>(subscriber: S) where S.Input == Output, S.Failure == Failure {
829+
self.upstream.subscribe(
830+
AnySubscriber(
831+
receiveSubscription: subscriber.receive(subscription:),
832+
receiveValue: subscriber.receive(_:),
833+
receiveCompletion: { [store = self.store] in
834+
subscriber.receive(completion: $0)
835+
_ = store
836+
}
837+
)
838+
)
839+
}
840+
841+
/// Returns the resulting publisher of a given key path.
842+
public subscript<Value: Equatable>(
843+
dynamicMember keyPath: KeyPath<State, Value>
844+
) -> StorePublisher<Value> {
845+
.init(store: self.store, upstream: self.upstream.map(keyPath).removeDuplicates())
846+
}
847+
}
848+
798849
/// The type returned from ``Store/send(_:)`` that represents the lifecycle of the effect
799850
/// started from sending an action.
800851
///

Sources/ComposableArchitecture/ViewStore.swift

Lines changed: 1 addition & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ public final class ViewStore<ViewState, ViewAction>: ObservableObject {
212212
/// `viewStore.publisher.sink` closures should be completely independent of each other. Later
213213
/// closures cannot assume that earlier ones have already run.
214214
public var publisher: StorePublisher<ViewState> {
215-
StorePublisher(viewStore: self)
215+
StorePublisher(store: self, upstream: self._state)
216216
}
217217

218218
/// The current state.
@@ -661,49 +661,6 @@ extension ViewStore where ViewState == Void {
661661
@available(*, deprecated, renamed: "StoreTask")
662662
public typealias ViewStoreTask = StoreTask
663663

664-
/// A publisher of store state.
665-
@dynamicMemberLookup
666-
public struct StorePublisher<State>: Publisher {
667-
public typealias Output = State
668-
public typealias Failure = Never
669-
670-
public let upstream: AnyPublisher<State, Never>
671-
public let viewStore: Any
672-
673-
fileprivate init<Action>(viewStore: ViewStore<State, Action>) {
674-
self.viewStore = viewStore
675-
self.upstream = viewStore._state.eraseToAnyPublisher()
676-
}
677-
678-
public func receive<S: Subscriber>(subscriber: S) where S.Input == Output, S.Failure == Failure {
679-
self.upstream.subscribe(
680-
AnySubscriber(
681-
receiveSubscription: subscriber.receive(subscription:),
682-
receiveValue: subscriber.receive(_:),
683-
receiveCompletion: { [viewStore = self.viewStore] in
684-
subscriber.receive(completion: $0)
685-
_ = viewStore
686-
}
687-
)
688-
)
689-
}
690-
691-
private init<P: Publisher>(
692-
upstream: P,
693-
viewStore: Any
694-
) where P.Output == Output, P.Failure == Failure {
695-
self.upstream = upstream.eraseToAnyPublisher()
696-
self.viewStore = viewStore
697-
}
698-
699-
/// Returns the resulting publisher of a given key path.
700-
public subscript<Value: Equatable>(
701-
dynamicMember keyPath: KeyPath<State, Value>
702-
) -> StorePublisher<Value> {
703-
.init(upstream: self.upstream.map(keyPath).removeDuplicates(), viewStore: self.viewStore)
704-
}
705-
}
706-
707664
private struct HashableWrapper<Value>: Hashable {
708665
let rawValue: Value
709666
static func == (lhs: Self, rhs: Self) -> Bool { false }

0 commit comments

Comments
 (0)