Skip to content

Commit c1acd01

Browse files
stephencelisp4checo
authored andcommitted
IfLetStore: ignore view store binding writes to nil state (#1879)
* `IfLetStore`: ignore view store binding writes to `nil` state * swift-format * wip * add test for filter --------- Co-authored-by: Brandon Williams <[email protected]> (cherry picked from commit 98af2adcb5a6186168a60dd1db834e39a34aa4e1) # Conflicts: # Sources/ComposableArchitecture/Store.swift # Sources/ComposableArchitecture/SwiftUI/IfLetStore.swift # Sources/ComposableArchitecture/ViewStore.swift # Tests/ComposableArchitectureTests/StoreTests.swift
1 parent 49d09bd commit c1acd01

File tree

5 files changed

+917
-825
lines changed

5 files changed

+917
-825
lines changed

Sources/ComposableArchitecture/Store.swift

Lines changed: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -315,10 +315,10 @@ public final class Store<State, Action> {
315315
self.threadCheck(status: .scope)
316316

317317
#if swift(>=5.7)
318-
return self.reducer.rescope(self, state: toChildState, action: fromChildAction)
318+
return self.reducer.rescope(self, state: toChildState, action: { fromChildAction($1) })
319319
#else
320320
return (self.scope ?? StoreScope(root: self))
321-
.rescope(self, state: toChildState, action: fromChildAction)
321+
.rescope(self, state: toChildState, action: { fromChildAction($1) })
322322
#endif
323323
}
324324

@@ -334,6 +334,19 @@ public final class Store<State, Action> {
334334
self.scope(state: toChildState, action: { $0 })
335335
}
336336

337+
@_spi(Internals) public func filter(
338+
_ isSent: @escaping (State, Action) -> Bool
339+
) -> Store<State, Action> {
340+
self.threadCheck(status: .scope)
341+
342+
#if swift(>=5.7)
343+
return self.reducer.rescope(self, state: { $0 }, action: { isSent($0, $1) ? $1 : nil })
344+
#else
345+
return (self.scope ?? StoreScope(root: self))
346+
.rescope(self, state: { $0 }, action: { isSent($0, $1) ? $1 : nil })
347+
#endif
348+
}
349+
337350
@_spi(Internals) public func send(
338351
_ action: Action,
339352
originatingFrom originatingAction: Action? = nil
@@ -386,24 +399,24 @@ public final class Store<State, Action> {
386399
}
387400
},
388401
completed: { [weak self] in
389-
self?.threadCheck(status: .effectCompletion(action))
390-
boxedTask.wrappedValue?.cancel()
391-
didComplete = true
402+
self?.threadCheck(status: .effectCompletion(action))
403+
boxedTask.wrappedValue?.cancel()
404+
didComplete = true
392405
self?.effectDisposables.removeValue(forKey: uuid)?.dispose()
393-
},
406+
},
394407
interrupted: { [weak self] in
395408
boxedTask.wrappedValue?.cancel()
396409
didComplete = true
397410
self?.effectDisposables.removeValue(forKey: uuid)?.dispose()
398-
}
411+
}
399412
)
400413

401414
let effectDisposable = CompositeDisposable()
402415
effectDisposable += producer.start(observer)
403416
effectDisposable += AnyDisposable { [weak self] in
404417
self?.threadCheck(status: .effectCompletion(action))
405418
self?.effectDisposables.removeValue(forKey: uuid)?.dispose()
406-
}
419+
}
407420

408421
if !didComplete {
409422
let task = Task<Void, Never> { @MainActor in
@@ -590,7 +603,7 @@ public typealias StoreOf<R: ReducerProtocol> = Store<R.State, R.Action>
590603
fileprivate func rescope<ChildState, ChildAction>(
591604
_ store: Store<State, Action>,
592605
state toChildState: @escaping (State) -> ChildState,
593-
action fromChildAction: @escaping (ChildAction) -> Action
606+
action fromChildAction: @escaping (ChildState, ChildAction) -> Action?
594607
) -> Store<ChildState, ChildAction> {
595608
(self as? any AnyScopedReducer ?? ScopedReducer(rootStore: store))
596609
.rescope(store, state: toChildState, action: fromChildAction)
@@ -603,7 +616,7 @@ public typealias StoreOf<R: ReducerProtocol> = Store<R.State, R.Action>
603616
let rootStore: Store<RootState, RootAction>
604617
let toScopedState: (RootState) -> ScopedState
605618
private let parentStores: [Any]
606-
let fromScopedAction: (ScopedAction) -> RootAction
619+
let fromScopedAction: (ScopedState, ScopedAction) -> RootAction?
607620
private(set) var isSending = false
608621

609622
@inlinable
@@ -612,14 +625,14 @@ public typealias StoreOf<R: ReducerProtocol> = Store<R.State, R.Action>
612625
self.rootStore = rootStore
613626
self.toScopedState = { $0 }
614627
self.parentStores = []
615-
self.fromScopedAction = { $0 }
628+
self.fromScopedAction = { $1 }
616629
}
617630

618631
@inlinable
619632
init(
620633
rootStore: Store<RootState, RootAction>,
621634
state toScopedState: @escaping (RootState) -> ScopedState,
622-
action fromScopedAction: @escaping (ScopedAction) -> RootAction,
635+
action fromScopedAction: @escaping (ScopedState, ScopedAction) -> RootAction?,
623636
parentStores: [Any]
624637
) {
625638
self.rootStore = rootStore
@@ -637,7 +650,7 @@ public typealias StoreOf<R: ReducerProtocol> = Store<R.State, R.Action>
637650
state = self.toScopedState(self.rootStore.state)
638651
self.isSending = false
639652
}
640-
if let task = self.rootStore.send(self.fromScopedAction(action)) {
653+
if let action = self.fromScopedAction(state, action), let task = self.rootStore.send(action) {
641654
return .fireAndForget { await task.cancellableValue }
642655
} else {
643656
return .none
@@ -649,7 +662,7 @@ public typealias StoreOf<R: ReducerProtocol> = Store<R.State, R.Action>
649662
func rescope<ScopedState, ScopedAction, RescopedState, RescopedAction>(
650663
_ store: Store<ScopedState, ScopedAction>,
651664
state toRescopedState: @escaping (ScopedState) -> RescopedState,
652-
action fromRescopedAction: @escaping (RescopedAction) -> ScopedAction
665+
action fromRescopedAction: @escaping (RescopedState, RescopedAction) -> ScopedAction?
653666
) -> Store<RescopedState, RescopedAction>
654667
}
655668

@@ -658,13 +671,13 @@ public typealias StoreOf<R: ReducerProtocol> = Store<R.State, R.Action>
658671
func rescope<ScopedState, ScopedAction, RescopedState, RescopedAction>(
659672
_ store: Store<ScopedState, ScopedAction>,
660673
state toRescopedState: @escaping (ScopedState) -> RescopedState,
661-
action fromRescopedAction: @escaping (RescopedAction) -> ScopedAction
674+
action fromRescopedAction: @escaping (RescopedState, RescopedAction) -> ScopedAction?
662675
) -> Store<RescopedState, RescopedAction> {
663-
let fromScopedAction = self.fromScopedAction as! (ScopedAction) -> RootAction
676+
let fromScopedAction = self.fromScopedAction as! (ScopedState, ScopedAction) -> RootAction?
664677
let reducer = ScopedReducer<RootState, RootAction, RescopedState, RescopedAction>(
665678
rootStore: self.rootStore,
666679
state: { _ in toRescopedState(store.state) },
667-
action: { fromScopedAction(fromRescopedAction($0)) },
680+
action: { fromRescopedAction($0, $1).flatMap { fromScopedAction(store.state.value, $0) } },
668681
parentStores: self.parentStores + [store]
669682
)
670683
let childStore = Store<RescopedState, RescopedAction>(
@@ -685,7 +698,7 @@ public typealias StoreOf<R: ReducerProtocol> = Store<R.State, R.Action>
685698
func rescope<ScopedState, ScopedAction, RescopedState, RescopedAction>(
686699
_ store: Store<ScopedState, ScopedAction>,
687700
state toRescopedState: @escaping (ScopedState) -> RescopedState,
688-
action fromRescopedAction: @escaping (RescopedAction) -> ScopedAction
701+
action fromRescopedAction: @escaping (RescopedState, RescopedAction) -> ScopedAction?
689702
) -> Store<RescopedState, RescopedAction>
690703
}
691704

@@ -694,12 +707,15 @@ public typealias StoreOf<R: ReducerProtocol> = Store<R.State, R.Action>
694707
let fromScopedAction: Any
695708

696709
init(root: Store<RootState, RootAction>) {
697-
self.init(root: root, fromScopedAction: { $0 })
710+
self.init(
711+
root: root,
712+
fromScopedAction: { (state: RootState, action: RootAction) -> RootAction? in action }
713+
)
698714
}
699715

700-
private init<ScopedAction>(
716+
private init<ScopedState, ScopedAction>(
701717
root: Store<RootState, RootAction>,
702-
fromScopedAction: @escaping (ScopedAction) -> RootAction
718+
fromScopedAction: @escaping (ScopedState, ScopedAction) -> RootAction?
703719
) {
704720
self.root = root
705721
self.fromScopedAction = fromScopedAction
@@ -708,17 +724,21 @@ public typealias StoreOf<R: ReducerProtocol> = Store<R.State, R.Action>
708724
func rescope<ScopedState, ScopedAction, RescopedState, RescopedAction>(
709725
_ scopedStore: Store<ScopedState, ScopedAction>,
710726
state toRescopedState: @escaping (ScopedState) -> RescopedState,
711-
action fromRescopedAction: @escaping (RescopedAction) -> ScopedAction
727+
action fromRescopedAction: @escaping (RescopedState, RescopedAction) -> ScopedAction?
712728
) -> Store<RescopedState, RescopedAction> {
713-
let fromScopedAction = self.fromScopedAction as! (ScopedAction) -> RootAction
729+
let fromScopedAction = self.fromScopedAction as! (ScopedState, ScopedAction) -> RootAction?
714730

715731
var isSending = false
716732
let rescopedStore = Store<RescopedState, RescopedAction>(
717733
initialState: toRescopedState(scopedStore.state),
718734
reducer: .init { rescopedState, rescopedAction, _ in
719735
isSending = true
720736
defer { isSending = false }
721-
let task = self.root.send(fromScopedAction(fromRescopedAction(rescopedAction)))
737+
guard
738+
let scopedAction = fromRescopedAction(rescopedState, rescopedAction),
739+
let rootAction = fromScopedAction(scopedStore.state.value, scopedAction)
740+
else { return .none }
741+
let task = self.root.send(rootAction)
722742
rescopedState = toRescopedState(scopedStore.state)
723743
if let task = task {
724744
return .fireAndForget { await task.cancellableValue }
@@ -736,7 +756,9 @@ public typealias StoreOf<R: ReducerProtocol> = Store<R.State, R.Action>
736756
}
737757
rescopedStore.scope = StoreScope<RootState, RootAction>(
738758
root: self.root,
739-
fromScopedAction: { fromScopedAction(fromRescopedAction($0)) }
759+
fromScopedAction: {
760+
fromRescopedAction($0, $1).flatMap { fromScopedAction(scopedStore.state.value, $0) }
761+
}
740762
)
741763
return rescopedStore
742764
}

0 commit comments

Comments
 (0)