Skip to content

Commit 5e0586d

Browse files
committed
Make ReactiveSwift versions performance improvements to Store and ViewStore
1 parent 65e4399 commit 5e0586d

File tree

5 files changed

+104
-115
lines changed

5 files changed

+104
-115
lines changed

Examples/CaseStudies/SwiftUICaseStudiesTests/02-Effects-CancellationTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class EffectsCancellationTests: XCTestCase {
3535
initialState: .init(),
3636
reducer: effectsCancellationReducer,
3737
environment: .init(
38-
fact: .init(fetch: { _ in Fail(error: FactClient.Error()).eraseToEffect() }),
38+
fact: .init(fetch: { _ in Effect(error: FactClient.Error()) }),
3939
mainQueue: ImmediateScheduler()
4040
)
4141
)

Sources/ComposableArchitecture/Internal/Deprecations.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ import CasePaths
33

44
import SwiftUI
55

6-
// NB: Deprecated after 0.20.0:
7-
6+
// NB: Deprecated after 0.21.0:
87
extension Reducer {
98
@available(*, deprecated, message: "Use the 'IdentifiedArray'-based version, instead")
109
public func forEach<GlobalState, GlobalAction, GlobalEnvironment>(
@@ -62,6 +61,7 @@ extension Reducer {
6261
}
6362
}
6463

64+
@available(iOS 13, macOS 10.15, macCatalyst 13, tvOS 13, watchOS 6, *)
6565
extension ForEachStore {
6666
@available(*, deprecated, message: "Use the 'IdentifiedArray'-based version, instead")
6767
public init<EachContent>(
@@ -76,7 +76,7 @@ extension ForEachStore {
7676
[ID], (Data.Index, EachAction), ForEach<[(offset: Int, element: ID)], ID, EachContent>
7777
>
7878
{
79-
let data = store.state.value
79+
let data = store.$state.value
8080
self.data = data
8181
self.content = {
8282
WithViewStore(store.scope(state: { $0.map { $0[keyPath: id] } })) { viewStore in

Sources/ComposableArchitecture/Store.swift

Lines changed: 26 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -293,12 +293,14 @@ public final class Store<State, Action> {
293293
},
294294
environment: ()
295295
)
296-
localStore.parentCancellable = self.state
297-
.dropFirst()
298-
.sink { [weak localStore] newValue in
296+
297+
localStore.parentDisposable = self.$state.producer
298+
.skip(first: 1)
299+
.startWithValues { [weak localStore] newValue in
299300
guard !isSending else { return }
300-
localStore?.state.value = toLocalState(newValue)
301-
}
301+
localStore?.$state.value = toLocalState(newValue)
302+
}
303+
302304
return localStore
303305
}
304306

@@ -364,13 +366,13 @@ public final class Store<State, Action> {
364366
}
365367

366368
func send(_ action: Action) {
367-
self.bufferedActions.append(action)
369+
self.bufferedActions.append(action)
368370
guard !self.isSending else { return }
369371

370-
self.isSending = true
371-
var currentState = self.state.value
372+
self.isSending = true
373+
var currentState = self.$state.value
372374
defer {
373-
self.state.value = currentState
375+
self.$state.value = currentState
374376
self.isSending = false
375377
}
376378

@@ -380,18 +382,25 @@ public final class Store<State, Action> {
380382

381383
var didComplete = false
382384
let uuid = UUID()
383-
let effectCancellable = effect.sink(
384-
receiveCompletion: { [weak self] _ in
385+
386+
let observer = Signal<Action, Never>.Observer(
387+
value: { [weak self] action in
388+
self?.send(action)
389+
},
390+
completed: { [weak self] in
385391
didComplete = true
386-
self?.effectCancellables[uuid] = nil
392+
self?.effectDisposables.removeValue(forKey: uuid)?.dispose()
387393
},
388-
receiveValue: { [weak self] action in
389-
self?.send(action)
390-
}
394+
interrupted: { [weak self] in
395+
didComplete = true
396+
self?.effectDisposables.removeValue(forKey: uuid)?.dispose()
397+
}
391398
)
392399

400+
let effectDisposable = effect.start(observer)
401+
393402
if !didComplete {
394-
self.effectDisposables[effectID] = effectDisposable
403+
self.effectDisposables[uuid] = effectDisposable
395404
} else {
396405
effectDisposable.dispose()
397406
}
@@ -413,52 +422,6 @@ public final class Store<State, Action> {
413422
self.parentDisposable?.dispose()
414423
self.effectDisposables.keys.forEach { id in
415424
self.effectDisposables.removeValue(forKey: id)?.dispose()
416-
}
417-
}
418-
}
419-
420-
/// A producer of store state.
421-
@dynamicMemberLookup
422-
public struct Produced<Value>: SignalProducerConvertible {
423-
private let _producer: Effect<Value, Never>
424-
private let comparator: (Value, Value) -> Bool
425-
426-
public var producer: Effect<Value, Never> {
427-
_producer.skipRepeats(comparator)
428-
}
429-
430-
init(
431-
by upstream: Effect<Value, Never>,
432-
isEqual: @escaping (Value, Value) -> Bool
433-
) {
434-
self._producer = upstream
435-
self.comparator = isEqual
436-
}
437-
438-
init(by upstream: Effect<Value, Never>) where Value: Equatable {
439-
self.init(by: upstream, isEqual: ==)
440-
}
441-
442-
/// Returns the resulting producer of a given key path.
443-
public subscript<LocalValue>(
444-
dynamicMember keyPath: KeyPath<Value, LocalValue>
445-
) -> Effect<LocalValue, Never> where LocalValue: Equatable {
446-
self.producer.map(keyPath).skipRepeats()
447-
}
448-
449-
/// Returns the resulting producer of a given key path.
450-
public subscript<LocalValue>(
451-
dynamicMember keyPath: KeyPath<Value, LocalValue>
452-
) -> Produced<LocalValue> where LocalValue: Equatable {
453-
Produced<LocalValue>(by: self.producer.map(keyPath).skipRepeats())
425+
}
454426
}
455427
}
456-
457-
@available(
458-
*, deprecated,
459-
message:
460-
"""
461-
Consider using `Produced<State>` instead, this typealias is added for backward compatibility and will be removed in the next major release.
462-
"""
463-
)
464-
public typealias StoreProducer<State> = Produced<State>

Sources/ComposableArchitecture/ViewStore.swift

Lines changed: 64 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -57,31 +57,14 @@ import SwiftUI
5757
/// made.
5858
@dynamicMemberLookup
5959
public final class ViewStore<State, Action> {
60-
/// A producer of state.
61-
public let produced: Produced<State>
62-
63-
@available(
64-
*, deprecated,
65-
message:
66-
"""
67-
Consider using `.produced` instead, this property exists for backwards compatibility and will be removed in the next major release.
68-
"""
69-
)
70-
public var producer: StoreProducer<State> { produced }
71-
72-
@available(
73-
*, deprecated,
74-
message:
75-
"""
76-
Consider using `.produced` instead, this property exists for backwards compatibility and will be removed in the next major release.
77-
"""
78-
)
79-
public var publisher: StoreProducer<State> { produced }
80-
internal var viewDisposable: Disposable?
60+
#if canImport(Combine)
61+
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
62+
public private(set) lazy var objectWillChange = ObservableObjectPublisher()
63+
#endif
8164

8265
private let _send: (Action) -> Void
83-
private let _state: CurrentValueSubject<State, Never>
84-
private var viewCancellable: AnyCancellable?
66+
fileprivate let _state: MutableProperty<State>
67+
private var viewDisposable: Disposable?
8568

8669
/// Initializes a view store from a store.
8770
///
@@ -93,34 +76,31 @@ public final class ViewStore<State, Action> {
9376
_ store: Store<State, Action>,
9477
removeDuplicates isDuplicate: @escaping (State, State) -> Bool
9578
) {
96-
let produced = Produced(by: store.$state.producer, isEqual: isDuplicate)
97-
self.produced = produced
98-
self.state = store.state
79+
self._state = MutableProperty(store.$state.value)
9980
self._send = store.send
100-
self.viewDisposable = produced.producer.startWithValues { [weak self] state in
101-
self?.state = state
102-
}
10381

104-
self.publisher = StorePublisher(self._state)
105-
self.viewCancellable = store.state
106-
.removeDuplicates(by: isDuplicate)
107-
.sink { [weak self] in
82+
self.viewDisposable = store.$state.producer
83+
.skipRepeats(isDuplicate)
84+
.startWithValues { [weak self] in
10885
guard let self = self else { return }
109-
#if canImport(Combine)
110-
if #available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) {
111-
self.objectWillChange.send()
112-
self._state.send($0)
113-
}
114-
#endif
115-
}
86+
#if canImport(Combine)
87+
if #available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) {
88+
self.objectWillChange.send()
89+
self._state.value = $0
90+
}
91+
#endif
92+
}
11693
}
11794

118-
#if canImport(Combine)
119-
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
120-
public lazy var objectWillChange = ObservableObjectPublisher()
121-
#endif
95+
/// A producer of state.
96+
public var produced: StoreProducer<State> {
97+
StoreProducer(viewStore: self)
98+
}
12299

123-
let _send: (Action) -> Void
100+
/// The current state.
101+
public var state: State {
102+
self._state.value
103+
}
124104

125105
/// Returns the resulting value of a given key path.
126106
public subscript<LocalState>(dynamicMember keyPath: KeyPath<State, LocalState>) -> LocalState {
@@ -303,3 +283,42 @@ extension ViewStore where State == Void {
303283
extension ViewStore: ObservableObject {
304284
}
305285
#endif
286+
287+
@dynamicMemberLookup
288+
public struct StoreProducer<State>: SignalProducerConvertible {
289+
public let upstream: Effect<State, Never>
290+
public let viewStore: Any
291+
292+
public var producer: Effect<State, Never> {
293+
upstream
294+
}
295+
296+
fileprivate init<Action>(viewStore: ViewStore<State, Action>) {
297+
self.viewStore = viewStore
298+
self.upstream = viewStore._state.producer
299+
}
300+
301+
private init(
302+
upstream: Effect<State, Never>,
303+
viewStore: Any
304+
) {
305+
self.upstream = upstream
306+
self.viewStore = viewStore
307+
}
308+
309+
/// Returns the resulting `StoreProducer` of a given key path.
310+
public subscript<LocalState>(
311+
dynamicMember keyPath: KeyPath<State, LocalState>
312+
) -> StoreProducer<LocalState>
313+
where LocalState: Equatable
314+
{
315+
.init(upstream: self.upstream.map(keyPath).skipRepeats(), viewStore: self.viewStore)
316+
}
317+
318+
/// Returns the resulting `SignalProducer` of a given key path.
319+
public subscript<LocalValue>(
320+
dynamicMember keyPath: KeyPath<Value, LocalValue>
321+
) -> Effect<LocalValue, Never> where LocalValue: Equatable {
322+
self.upstream.map(keyPath).skipRepeats()
323+
}
324+
}

Tests/ComposableArchitectureTests/ViewStoreTests.swift

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1+
#if canImport(Combine)
2+
import Combine
3+
#endif
14
import ComposableArchitecture
25
import ReactiveSwift
36
import XCTest
47

8+
59
final class ViewStoreTests: XCTestCase {
10+
var cancellables: Set<AnyCancellable> = []
11+
612
override func setUp() {
713
super.setUp()
814
equalityChecks = 0
@@ -84,9 +90,8 @@ final class ViewStoreTests: XCTestCase {
8490

8591
var results: [Int] = []
8692

87-
viewStore.publisher
88-
.sink { _ in results.append(viewStore.state) }
89-
.store(in: &self.cancellables)
93+
viewStore.produced.producer
94+
.startWithValues { _ in results.append(viewStore.state) }
9095

9196
viewStore.send(())
9297
viewStore.send(())
@@ -95,6 +100,7 @@ final class ViewStoreTests: XCTestCase {
95100
XCTAssertEqual([0, 1, 2, 3], results)
96101
}
97102

103+
#if canImport(Combine)
98104
func testWillSet() {
99105
let reducer = Reducer<Int, Void, Void> { count, _, _ in
100106
count += 1
@@ -116,6 +122,7 @@ final class ViewStoreTests: XCTestCase {
116122

117123
XCTAssertEqual([0, 1, 2], results)
118124
}
125+
#endif
119126
}
120127

121128
private struct State: Equatable {

0 commit comments

Comments
 (0)