Skip to content

Commit 14c3162

Browse files
authored
feat(Produced): Mapping API improvements (#28)
* feat: Produced state mappings * feat(Produced): Responsibility improvements * fix: Merge issues
1 parent 81d795b commit 14c3162

File tree

3 files changed

+82
-34
lines changed

3 files changed

+82
-34
lines changed

Sources/ComposableArchitecture/Store.swift

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ import ReactiveSwift
99
public final class Store<State, Action> {
1010
@MutableProperty
1111
private(set) var state: State
12-
12+
1313
private var isSending = false
1414
private let reducer: (inout State, Action) -> Effect<Action, Never>
1515
private var synchronousActionsToSend: [Action] = []
1616
private var bufferedActions: [Action] = []
1717
internal var effectDisposables: [UUID: Disposable] = [:]
1818
internal var parentDisposable: Disposable?
19-
19+
2020
/// Initializes a store from an initial state, a reducer, and an environment.
2121
///
2222
/// - Parameters:
@@ -33,7 +33,7 @@ public final class Store<State, Action> {
3333
reducer: { reducer.run(&$0, $1, environment) }
3434
)
3535
}
36-
36+
3737
/// Scopes the store to one that exposes local state and actions.
3838
///
3939
/// This can be useful for deriving new stores to hand to child views in an application. For
@@ -179,7 +179,7 @@ public final class Store<State, Action> {
179179
}
180180
return localStore
181181
}
182-
182+
183183
/// Scopes the store to one that exposes local state.
184184
///
185185
/// - Parameter toLocalState: A function that transforms `State` into `LocalState`.
@@ -189,7 +189,7 @@ public final class Store<State, Action> {
189189
) -> Store<LocalState, Action> {
190190
self.scope(state: toLocalState, action: { $0 })
191191
}
192-
192+
193193
/// Scopes the store to a producer of stores of more local state and local actions.
194194
///
195195
/// - Parameters:
@@ -201,14 +201,14 @@ public final class Store<State, Action> {
201201
state toLocalState: @escaping (Effect<State, Never>) -> Effect<LocalState, Never>,
202202
action fromLocalAction: @escaping (LocalAction) -> Action
203203
) -> Effect<Store<LocalState, LocalAction>, Never> {
204-
204+
205205
func extractLocalState(_ state: State) -> LocalState? {
206206
var localState: LocalState?
207207
_ = toLocalState(Effect(value: state))
208208
.startWithValues { localState = $0 }
209209
return localState
210210
}
211-
211+
212212
return toLocalState(self.$state.producer)
213213
.map { localState in
214214
let localStore = Store<LocalState, LocalAction>(
@@ -219,16 +219,14 @@ public final class Store<State, Action> {
219219
return .none
220220
}
221221
)
222-
223-
localStore.parentDisposable = self.$state.producer.startWithValues {
224-
[weak localStore] state in
222+
localStore.parentDisposable = self.$state.producer.startWithValues { [weak localStore] state in
225223
guard let localStore = localStore else { return }
226224
localStore.state = extractLocalState(state) ?? localStore.state
227225
}
228226
return localStore
229227
}
230228
}
231-
229+
232230
/// Scopes the store to a producer of stores of more local state and local actions.
233231
///
234232
/// - Parameter toLocalState: A function that transforms a producer of `State` into a producer
@@ -240,30 +238,29 @@ public final class Store<State, Action> {
240238
) -> Effect<Store<LocalState, Action>, Never> {
241239
self.producerScope(state: toLocalState, action: { $0 })
242240
}
243-
241+
244242
func send(_ action: Action) {
245243
if !self.isSending {
246244
self.synchronousActionsToSend.append(action)
247245
} else {
248246
self.bufferedActions.append(action)
249247
return
250248
}
251-
249+
252250
while !self.synchronousActionsToSend.isEmpty || !self.bufferedActions.isEmpty {
253251
let action =
254252
!self.synchronousActionsToSend.isEmpty
255253
? self.synchronousActionsToSend.removeFirst()
256254
: self.bufferedActions.removeFirst()
257-
255+
258256
self.isSending = true
259257
let effect = self.reducer(&self.state, action)
260258
self.isSending = false
261-
259+
262260
var didComplete = false
263261
let effectID = UUID()
264262

265263
var isProcessingEffects = true
266-
267264
let observer = Signal<Action, Never>.Observer(
268265
value: { [weak self] action in
269266
if isProcessingEffects {
@@ -284,34 +281,34 @@ public final class Store<State, Action> {
284281
)
285282
let effectDisposable = effect.start(observer)
286283
isProcessingEffects = false
287-
284+
288285
if !didComplete {
289286
self.effectDisposables[effectID] = effectDisposable
290287
} else {
291288
effectDisposable.dispose()
292289
}
293290
}
294291
}
295-
292+
296293
/// Returns a "stateless" store by erasing state to `Void`.
297294
public var stateless: Store<Void, Action> {
298295
self.scope(state: { _ in () })
299296
}
300-
297+
301298
/// Returns an "actionless" store by erasing action to `Never`.
302299
public var actionless: Store<State, Never> {
303300
func absurd<A>(_ never: Never) -> A {}
304301
return self.scope(state: { $0 }, action: absurd)
305302
}
306-
303+
307304
private init(
308305
initialState: State,
309306
reducer: @escaping (inout State, Action) -> Effect<Action, Never>
310307
) {
311308
self.reducer = reducer
312309
self.state = initialState
313310
}
314-
311+
315312
deinit {
316313
self.parentDisposable?.dispose()
317314
self.effectDisposables.keys.forEach { id in
@@ -323,24 +320,44 @@ public final class Store<State, Action> {
323320
/// A producer of store state.
324321
@dynamicMemberLookup
325322
public struct Produced<Value>: SignalProducerConvertible {
326-
public let producer: Effect<Value, Never>
327-
328-
init(by upstream: Effect<Value, Never>) {
329-
self.producer = upstream
323+
private let _producer: Effect<Value, Never>
324+
private let comparator: (Value, Value) -> Bool
325+
326+
public var producer: Effect<Value, Never> {
327+
_producer.skipRepeats(comparator)
330328
}
331-
329+
330+
init(
331+
by upstream: Effect<Value, Never>,
332+
isEqual: @escaping (Value, Value) -> Bool
333+
) {
334+
self._producer = upstream
335+
self.comparator = isEqual
336+
}
337+
338+
init(by upstream: Effect<Value, Never>) where Value: Equatable {
339+
self.init(by: upstream, isEqual: ==)
340+
}
341+
332342
/// Returns the resulting producer of a given key path.
333343
public subscript<LocalValue>(
334344
dynamicMember keyPath: KeyPath<Value, LocalValue>
335345
) -> Effect<LocalValue, Never> where LocalValue: Equatable {
336346
self.producer.map(keyPath).skipRepeats()
337347
}
348+
349+
/// Returns the resulting producer of a given key path.
350+
public subscript<LocalValue>(
351+
dynamicMember keyPath: KeyPath<Value, LocalValue>
352+
) -> Produced<LocalValue> where LocalValue: Equatable {
353+
Produced<LocalValue>(by: self.producer.map(keyPath).skipRepeats())
354+
}
338355
}
339356

340357
@available(
341-
*, deprecated,
342-
message:
343-
"""
358+
*, deprecated,
359+
message:
360+
"""
344361
Consider using `Produced<State>` instead, this typealias is added for backward compatibility and will be removed in the next major release.
345362
"""
346363
)

Sources/ComposableArchitecture/ViewStore.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public final class ViewStore<State, Action> {
6868
"""
6969
)
7070
public var publisher: StoreProducer<State> { produced }
71-
71+
7272
internal var viewDisposable: Disposable?
7373

7474
/// Initializes a view store from a store.
@@ -81,11 +81,11 @@ public final class ViewStore<State, Action> {
8181
_ store: Store<State, Action>,
8282
removeDuplicates isDuplicate: @escaping (State, State) -> Bool
8383
) {
84-
let producer = store.$state.producer.skipRepeats(isDuplicate)
85-
self.produced = Produced(by: producer)
84+
let produced = Produced(by: store.$state.producer, isEqual: isDuplicate)
85+
self.produced = produced
8686
self.state = store.state
8787
self._send = store.send
88-
self.viewDisposable = producer.startWithValues { [weak self] state in
88+
self.viewDisposable = produced.producer.startWithValues { [weak self] state in
8989
self?.state = state
9090
}
9191
}
@@ -259,7 +259,7 @@ public final class ViewStore<State, Action> {
259259
self.binding(send: { _ in action })
260260
}
261261
#endif
262-
262+
263263
deinit {
264264
viewDisposable?.dispose()
265265
}

Tests/ComposableArchitectureTests/StoreTests.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,37 @@ import XCTest
44
@testable import ComposableArchitecture
55

66
final class StoreTests: XCTestCase {
7+
8+
func testProducedMapping() {
9+
struct ChildState: Equatable {
10+
var value: Int = 0
11+
}
12+
struct ParentState: Equatable {
13+
var child: ChildState = .init()
14+
}
15+
16+
let store = Store<ParentState, Void>(
17+
initialState: ParentState(),
18+
reducer: Reducer { state, _, _ in
19+
state.child.value += 1
20+
return .none
21+
},
22+
environment: ()
23+
)
24+
25+
let viewStore = ViewStore(store)
26+
var values: [Int] = []
27+
28+
viewStore.produced.child.value.startWithValues { value in
29+
values.append(value)
30+
}
31+
32+
viewStore.send(())
33+
viewStore.send(())
34+
viewStore.send(())
35+
36+
XCTAssertEqual(values, [0, 1, 2, 3])
37+
}
738

839
func testEffectDisposablesDeinitialization() {
940
enum Action {

0 commit comments

Comments
 (0)