Skip to content

Commit 5ca2b43

Browse files
Added multicast to ViewStore publisher to reduce equality checks (#624)
* Added multicast to ViewStore publisher to reduce equality checks * Replaced multicast with ViewStore CurrentValueSubject * Added guard check to weak self * Clean up and add test. Co-authored-by: Brandon Williams <[email protected]>
1 parent 7bdd351 commit 5ca2b43

File tree

3 files changed

+51
-25
lines changed

3 files changed

+51
-25
lines changed

Sources/ComposableArchitecture/Store.swift

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -416,27 +416,24 @@ public struct StorePublisher<State>: Publisher {
416416
public typealias Output = State
417417
public typealias Failure = Never
418418

419-
private let isDuplicate: (State, State) -> Bool
420419
public let upstream: AnyPublisher<State, Never>
421420

422421
public func receive<S>(subscriber: S)
423422
where S: Subscriber, Failure == S.Failure, Output == S.Input {
424-
self.upstream.removeDuplicates(by: isDuplicate).subscribe(subscriber)
423+
self.upstream.subscribe(subscriber)
425424
}
426425

427426
init<P>(
428-
_ upstream: P,
429-
removeDuplicates isDuplicate: @escaping (State, State) -> Bool
427+
_ upstream: P
430428
) where P: Publisher, Failure == P.Failure, Output == P.Output {
431429
self.upstream = upstream.eraseToAnyPublisher()
432-
self.isDuplicate = isDuplicate
433430
}
434431

435432
/// Returns the resulting publisher of a given key path.
436433
public subscript<LocalState>(
437434
dynamicMember keyPath: KeyPath<State, LocalState>
438435
) -> StorePublisher<LocalState>
439436
where LocalState: Equatable {
440-
.init(self.upstream.map(keyPath), removeDuplicates: ==)
437+
.init(self.upstream.map(keyPath).removeDuplicates())
441438
}
442439
}

Sources/ComposableArchitecture/ViewStore.swift

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,13 @@ public final class ViewStore<State, Action>: ObservableObject {
5555
/// A publisher of state.
5656
public let publisher: StorePublisher<State>
5757

58-
private var viewCancellable: AnyCancellable?
59-
6058
// N.B. `ViewStore` does not use a `@Published` property, so `objectWillChange`
6159
// won't be synthesized automatically. To work around issues on iOS 13 we explicitly declare it.
6260
public private(set) lazy var objectWillChange = ObservableObjectPublisher()
6361

64-
/// The current state.
65-
public var state: State { self.store.state.value }
66-
67-
private let store: Store<State, Action>
62+
private let _send: (Action) -> Void
63+
private let _state: CurrentValueSubject<State, Never>
64+
private var viewCancellable: AnyCancellable?
6865

6966
/// Initializes a view store from a store.
7067
///
@@ -76,17 +73,27 @@ public final class ViewStore<State, Action>: ObservableObject {
7673
_ store: Store<State, Action>,
7774
removeDuplicates isDuplicate: @escaping (State, State) -> Bool
7875
) {
79-
self.publisher = StorePublisher(store.state, removeDuplicates: isDuplicate)
80-
self.store = store
76+
self._send = store.send
77+
self._state = CurrentValueSubject(store.state.value)
78+
79+
self.publisher = StorePublisher(self._state)
8180
self.viewCancellable = store.state
82-
.dropFirst()
8381
.removeDuplicates(by: isDuplicate)
84-
.sink { [weak self] _ in self?.objectWillChange.send() }
82+
.sink { [weak self] in
83+
guard let self = self else { return }
84+
self.objectWillChange.send()
85+
self._state.send($0)
86+
}
87+
}
88+
89+
/// The current state.
90+
public var state: State {
91+
self._state.value
8592
}
8693

8794
/// Returns the resulting value of a given key path.
8895
public subscript<LocalState>(dynamicMember keyPath: KeyPath<State, LocalState>) -> LocalState {
89-
self.state[keyPath: keyPath]
96+
self._state.value[keyPath: keyPath]
9097
}
9198

9299
/// Sends an action to the store.
@@ -99,7 +106,7 @@ public final class ViewStore<State, Action>: ObservableObject {
99106
///
100107
/// - Parameter action: An action.
101108
public func send(_ action: Action) {
102-
self.store.send(action)
109+
self._send(action)
103110
}
104111

105112
/// Derives a binding from the store that prevents direct writes to state and instead sends
@@ -135,7 +142,7 @@ public final class ViewStore<State, Action>: ObservableObject {
135142
send localStateToViewAction: @escaping (LocalState) -> Action
136143
) -> Binding<LocalState> {
137144
Binding(
138-
get: { get(self.state) },
145+
get: { get(self._state.value) },
139146
set: { newLocalState, transaction in
140147
if transaction.animation != nil {
141148
withTransaction(transaction) {

Tests/ComposableArchitectureTests/ViewStoreTests.swift

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,16 @@ final class ViewStoreTests: XCTestCase {
6464
XCTAssertEqual(0, subEqualityChecks)
6565
viewStore4.send(())
6666
XCTAssertEqual(4, equalityChecks)
67+
XCTAssertEqual(4, subEqualityChecks)
68+
viewStore4.send(())
69+
XCTAssertEqual(8, equalityChecks)
6770
XCTAssertEqual(8, subEqualityChecks)
6871
viewStore4.send(())
6972
XCTAssertEqual(12, equalityChecks)
70-
XCTAssertEqual(20, subEqualityChecks)
71-
viewStore4.send(())
72-
XCTAssertEqual(20, equalityChecks)
73-
XCTAssertEqual(32, subEqualityChecks)
73+
XCTAssertEqual(12, subEqualityChecks)
7474
viewStore4.send(())
75-
XCTAssertEqual(28, equalityChecks)
76-
XCTAssertEqual(44, subEqualityChecks)
75+
XCTAssertEqual(16, equalityChecks)
76+
XCTAssertEqual(16, subEqualityChecks)
7777
}
7878

7979
func testAccessViewStoreStateInPublisherSink() {
@@ -97,6 +97,28 @@ final class ViewStoreTests: XCTestCase {
9797

9898
XCTAssertEqual([0, 1, 2, 3], results)
9999
}
100+
101+
func testWillSet() {
102+
let reducer = Reducer<Int, Void, Void> { count, _, _ in
103+
count += 1
104+
return .none
105+
}
106+
107+
let store = Store(initialState: 0, reducer: reducer, environment: ())
108+
let viewStore = ViewStore(store)
109+
110+
var results: [Int] = []
111+
112+
viewStore.objectWillChange
113+
.sink { _ in results.append(viewStore.state) }
114+
.store(in: &self.cancellables)
115+
116+
viewStore.send(())
117+
viewStore.send(())
118+
viewStore.send(())
119+
120+
XCTAssertEqual([0, 1, 2], results)
121+
}
100122
}
101123

102124
private struct State: Equatable {

0 commit comments

Comments
 (0)