Skip to content

Commit d4cb424

Browse files
Joseph CherryMalcolm Jarvis
Joseph Cherry
authored and
Malcolm Jarvis
committed
Add automatic skipRepeats for Equatable substate selection (ReSwift#300)
overrides select to skip repeats when substate is equatable Fixes ReSwift#298 # Conflicts: # CHANGELOG.md # ReSwiftTests/StoreSubscriberTests.swift # ReSwift/CoreTypes/Store.swift
1 parent 3071038 commit d4cb424

File tree

6 files changed

+173
-27
lines changed

6 files changed

+173
-27
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- Fix retain cycle in SubscriptionBox (#278) - @mjarvis, @DivineDominion
66
- Fix bug where using skipRepeats with optional substate would not notify when the substate became nil #55655 - @Ben-G
7+
- Add automatic skipRepeats for Equatable substate selection (#300) - @JoeCherry
78

89
# 4.0.0
910

ReSwift.xcodeproj/project.pbxproj

+4
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
73F39F381D3EE46A00DFFE62 /* StoreSubscriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73F39F371D3EE46A00DFFE62 /* StoreSubscriptionTests.swift */; };
8484
73F39F391D3EE46A00DFFE62 /* StoreSubscriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73F39F371D3EE46A00DFFE62 /* StoreSubscriptionTests.swift */; };
8585
73F39F3A1D3EE46A00DFFE62 /* StoreSubscriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73F39F371D3EE46A00DFFE62 /* StoreSubscriptionTests.swift */; };
86+
759801931FB16D2D006EDE17 /* AutomaticallySkipRepeatsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 759801921FB16D2D006EDE17 /* AutomaticallySkipRepeatsTests.swift */; };
8687
81BCBECE1C63167A00AA4F03 /* Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81BCBECD1C63167A00AA4F03 /* Subscription.swift */; };
8788
81BCBECF1C63167A00AA4F03 /* Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81BCBECD1C63167A00AA4F03 /* Subscription.swift */; };
8889
81BCBED01C63167A00AA4F03 /* Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81BCBECD1C63167A00AA4F03 /* Subscription.swift */; };
@@ -160,6 +161,7 @@
160161
73E545D91D22BCD600D114E8 /* XCTest+Assertions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "XCTest+Assertions.swift"; sourceTree = "<group>"; };
161162
73F39F331D3EE3C300DFFE62 /* StoreDispatchTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoreDispatchTests.swift; sourceTree = "<group>"; };
162163
73F39F371D3EE46A00DFFE62 /* StoreSubscriptionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoreSubscriptionTests.swift; sourceTree = "<group>"; };
164+
759801921FB16D2D006EDE17 /* AutomaticallySkipRepeatsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutomaticallySkipRepeatsTests.swift; sourceTree = "<group>"; };
163165
81BCBECD1C63167A00AA4F03 /* Subscription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Subscription.swift; sourceTree = "<group>"; };
164166
8DA430622E3D093002316DB5 /* Pods-SwiftLintIntegration.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftLintIntegration.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftLintIntegration/Pods-SwiftLintIntegration.debug.xcconfig"; sourceTree = "<group>"; };
165167
B5C08F1806830A13C9006A27 /* libPods-SwiftLintIntegration.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-SwiftLintIntegration.a"; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -301,6 +303,7 @@
301303
621C068B1C278BEF008029AE /* TypeHelperTests.swift */,
302304
73E545D91D22BCD600D114E8 /* XCTest+Assertions.swift */,
303305
73723E031D30AEF3006139F0 /* XCTest+Compatibility.swift */,
306+
759801921FB16D2D006EDE17 /* AutomaticallySkipRepeatsTests.swift */,
304307
);
305308
path = ReSwiftTests;
306309
sourceTree = "<group>";
@@ -854,6 +857,7 @@
854857
73F39F381D3EE46A00DFFE62 /* StoreSubscriptionTests.swift in Sources */,
855858
621C068C1C278BEF008029AE /* TypeHelperTests.swift in Sources */,
856859
259737EA1C2C611600869B8F /* StoreMiddlewareTests.swift in Sources */,
860+
759801931FB16D2D006EDE17 /* AutomaticallySkipRepeatsTests.swift in Sources */,
857861
);
858862
runOnlyForDeploymentPostprocessing = 0;
859863
};

ReSwift/CoreTypes/Store.swift

+37-14
Original file line numberDiff line numberDiff line change
@@ -67,27 +67,17 @@ open class Store<State: StateType>: StoreType {
6767
}
6868
}
6969

70-
open func subscribe<S: StoreSubscriber>(_ subscriber: S)
71-
where S.StoreSubscriberStateType == State {
72-
_ = subscribe(subscriber, transform: nil)
73-
}
74-
75-
open func subscribe<SelectedState, S: StoreSubscriber>(
76-
_ subscriber: S, transform: ((Subscription<State>) -> Subscription<SelectedState>)?
77-
) where S.StoreSubscriberStateType == SelectedState
70+
fileprivate func _subscribe<SelectedState, S: StoreSubscriber>(
71+
_ subscriber: S, originalSubscription: Subscription<State>,
72+
transformedSubscription: Subscription<SelectedState>?)
73+
where S.StoreSubscriberStateType == SelectedState
7874
{
7975
// If the same subscriber is already registered with the store, replace the existing
8076
// subscription with the new one.
8177
if let index = subscriptions.index(where: { $0.subscriber === subscriber }) {
8278
subscriptions.remove(at: index)
8379
}
8480

85-
// Create a subscription for the new subscriber.
86-
let originalSubscription = Subscription<State>()
87-
// Call the optional transformation closure. This allows callers to modify
88-
// the subscription, e.g. in order to subselect parts of the store's state.
89-
let transformedSubscription = transform?(originalSubscription)
90-
9181
let subscriptionBox = self.subscriptionBox(
9282
originalSubscription: originalSubscription,
9383
transformedSubscription: transformedSubscription,
@@ -101,6 +91,25 @@ open class Store<State: StateType>: StoreType {
10191
}
10292
}
10393

94+
open func subscribe<S: StoreSubscriber>(_ subscriber: S)
95+
where S.StoreSubscriberStateType == State {
96+
_ = subscribe(subscriber, transform: nil)
97+
}
98+
99+
open func subscribe<SelectedState, S: StoreSubscriber>(
100+
_ subscriber: S, transform: ((Subscription<State>) -> Subscription<SelectedState>)?
101+
) where S.StoreSubscriberStateType == SelectedState
102+
{
103+
// Create a subscription for the new subscriber.
104+
let originalSubscription = Subscription<State>()
105+
// Call the optional transformation closure. This allows callers to modify
106+
// the subscription, e.g. in order to subselect parts of the store's state.
107+
let transformedSubscription = transform?(originalSubscription)
108+
109+
_subscribe(subscriber, originalSubscription: originalSubscription,
110+
transformedSubscription: transformedSubscription)
111+
}
112+
104113
internal func subscriptionBox<T>(
105114
originalSubscription: Subscription<State>,
106115
transformedSubscription: Subscription<T>?,
@@ -182,4 +191,18 @@ extension Store where State: Equatable {
182191
where S.StoreSubscriberStateType == State {
183192
_ = subscribe(subscriber, transform: { $0.skipRepeats() })
184193
}
194+
195+
open func subscribe<SelectedState: Equatable, S: StoreSubscriber>(
196+
_ subscriber: S, transform: ((Subscription<State>) -> Subscription<SelectedState>)?
197+
) where S.StoreSubscriberStateType == SelectedState
198+
{
199+
let originalSubscription = Subscription<State>()
200+
201+
var transformedSubscription = transform?(originalSubscription)
202+
transformedSubscription = transformedSubscription?.skipRepeats()
203+
204+
_subscribe(subscriber,
205+
originalSubscription: originalSubscription,
206+
transformedSubscription: transformedSubscription)
207+
}
185208
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
//
2+
// AutomaticallySkipRepeatsTests.swift
3+
// ReSwift
4+
//
5+
// Created by Daniel Martín Prieto on 03/11/2017.
6+
// Copyright © 2017 Benjamin Encz. All rights reserved.
7+
//
8+
import XCTest
9+
import ReSwift
10+
11+
class AutomaticallySkipRepeatsTests: XCTestCase {
12+
13+
private var store: Store<State>!
14+
fileprivate var subscriptionUpdates: Int = 0
15+
16+
override func setUp() {
17+
super.setUp()
18+
// Put setup code here. This method is called before the invocation of each test method in the class.
19+
store = Store<State>(reducer: reducer, state: nil)
20+
subscriptionUpdates = 0
21+
}
22+
23+
override func tearDown() {
24+
// Put teardown code here. This method is called after the invocation of each test method in the class.
25+
store = nil
26+
subscriptionUpdates = 0
27+
super.tearDown()
28+
}
29+
30+
func testInitialSubscription() {
31+
store.subscribe(self) { $0.select { $0.name } }
32+
XCTAssertEqual(self.subscriptionUpdates, 1)
33+
}
34+
35+
func testDispatchUnrelatedActionWithExplicitSkipRepeats() {
36+
store.subscribe(self) { $0.select { $0.name }.skipRepeats() }
37+
XCTAssertEqual(self.subscriptionUpdates, 1)
38+
store.dispatch(ChangeAge(newAge: 30))
39+
XCTAssertEqual(self.subscriptionUpdates, 1)
40+
}
41+
42+
func testDispatchUnrelatedActionWithoutExplicitSkipRepeats() {
43+
store.subscribe(self) { $0.select { $0.name } }
44+
XCTAssertEqual(self.subscriptionUpdates, 1)
45+
store.dispatch(ChangeAge(newAge: 30))
46+
XCTAssertEqual(self.subscriptionUpdates, 1)
47+
}
48+
49+
}
50+
51+
extension AutomaticallySkipRepeatsTests: StoreSubscriber {
52+
func newState(state: String) {
53+
subscriptionUpdates += 1
54+
}
55+
}
56+
57+
private struct State: StateType {
58+
let age: Int
59+
let name: String
60+
}
61+
62+
extension State: Equatable {
63+
static func == (lhs: State, rhs: State) -> Bool {
64+
return lhs.age == rhs.age && lhs.name == rhs.name
65+
}
66+
}
67+
68+
struct ChangeAge: Action {
69+
let newAge: Int
70+
}
71+
72+
private let initialState = State(age: 29, name: "Daniel")
73+
74+
private func reducer(action: Action, state: State?) -> State {
75+
let defaultState = state ?? initialState
76+
switch action {
77+
case let changeAge as ChangeAge:
78+
return State(age: changeAge.newAge, name: defaultState.name)
79+
default:
80+
return defaultState
81+
}
82+
}

ReSwiftTests/StoreSubscriberTests.swift

+9-13
Original file line numberDiff line numberDiff line change
@@ -124,31 +124,28 @@ class StoreSubscriberTests: XCTestCase {
124124
XCTAssertEqual(subscriber.newStateCallCount, 1)
125125
}
126126

127-
/**
128-
it skips repeated state values by default when the selected substate is `Equatable`.
129-
*/
130-
func testSkipsStateUpdatesForEquatableSubstatesByDefault() {
131-
let reducer = TestValueStringReducer()
132-
let state = TestStringAppState()
127+
func testPassesOnDuplicateSubstateUpdatesByDefault() {
128+
let reducer = TestNonEquatableReducer()
129+
let state = TestNonEquatable()
133130
let store = Store(reducer: reducer.handleAction, state: state)
134-
let subscriber = TestFilteredSubscriber<String>()
131+
let subscriber = TestFilteredSubscriber<NonEquatable>()
135132

136133
store.subscribe(subscriber) {
137134
$0.select { $0.testValue }
138135
}
139136

140-
XCTAssertEqual(subscriber.receivedValue, "Initial")
137+
XCTAssertEqual(subscriber.receivedValue.testValue, "Initial")
141138

142-
store.dispatch(SetValueStringAction("Initial"))
139+
store.dispatch(SetNonEquatableAction(NonEquatable()))
143140

144-
XCTAssertEqual(subscriber.receivedValue, "Initial")
145-
XCTAssertEqual(subscriber.newStateCallCount, 1)
141+
XCTAssertEqual(subscriber.receivedValue.testValue, "Initial")
142+
XCTAssertEqual(subscriber.newStateCallCount, 2)
146143
}
147144

148145
func testSkipsStateUpdatesForEquatableStateByDefault() {
149146
let reducer = TestValueStringReducer()
150147
let state = TestStringAppState()
151-
let store = Store(reducer: reducer.handleAction, state: state)
148+
let store = Store(reducer: reducer.handleAction, state: state, middleware: [])
152149
let subscriber = TestFilteredSubscriber<TestStringAppState>()
153150

154151
store.subscribe(subscriber)
@@ -160,7 +157,6 @@ class StoreSubscriberTests: XCTestCase {
160157
XCTAssertEqual(subscriber.receivedValue.testValue, "Initial")
161158
XCTAssertEqual(subscriber.newStateCallCount, 1)
162159
}
163-
164160
}
165161

166162
class TestFilteredSubscriber<T>: StoreSubscriber {

ReSwiftTests/TestFakes.swift

+40
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,22 @@ extension TestStringAppState: Equatable {
3131
}
3232
}
3333

34+
struct TestNonEquatable: StateType {
35+
var testValue: NonEquatable
36+
37+
init() {
38+
testValue = NonEquatable()
39+
}
40+
}
41+
42+
struct NonEquatable {
43+
var testValue: String
44+
45+
init() {
46+
testValue = "Initial"
47+
}
48+
}
49+
3450
struct TestCustomAppState: StateType {
3551
var substate: TestCustomSubstate
3652

@@ -108,6 +124,15 @@ struct SetCustomSubstateAction: StandardActionConvertible {
108124
}
109125
}
110126

127+
struct SetNonEquatableAction: Action {
128+
var value: NonEquatable
129+
static let type = "SetNonEquatableAction"
130+
131+
init (_ value: NonEquatable) {
132+
self.value = value
133+
}
134+
}
135+
111136
struct TestReducer {
112137
func handleAction(action: Action, state: TestAppState?) -> TestAppState {
113138
var state = state ?? TestAppState()
@@ -150,6 +175,21 @@ struct TestCustomAppStateReducer {
150175
}
151176
}
152177

178+
struct TestNonEquatableReducer {
179+
func handleAction(action: Action, state: TestNonEquatable?) ->
180+
TestNonEquatable {
181+
var state = state ?? TestNonEquatable()
182+
183+
switch action {
184+
case let action as SetNonEquatableAction:
185+
state.testValue = action.value
186+
return state
187+
default:
188+
return state
189+
}
190+
}
191+
}
192+
153193
class TestStoreSubscriber<T>: StoreSubscriber {
154194
var receivedStates: [T] = []
155195

0 commit comments

Comments
 (0)