Skip to content

Commit

Permalink
Add automatic skipRepeats for Equatable substate selection (#300)
Browse files Browse the repository at this point in the history
overrides select to skip repeats when substate is equatable

Fixes #298
# Conflicts:
#	CHANGELOG.md
#	ReSwiftTests/StoreSubscriberTests.swift
#	ReSwift/CoreTypes/Store.swift
  • Loading branch information
Joseph Cherry authored and Malcolm Jarvis committed Dec 15, 2017
1 parent 3071038 commit d4cb424
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 27 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -4,6 +4,7 @@

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

# 4.0.0

Expand Down
4 changes: 4 additions & 0 deletions ReSwift.xcodeproj/project.pbxproj
Expand Up @@ -83,6 +83,7 @@
73F39F381D3EE46A00DFFE62 /* StoreSubscriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73F39F371D3EE46A00DFFE62 /* StoreSubscriptionTests.swift */; };
73F39F391D3EE46A00DFFE62 /* StoreSubscriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73F39F371D3EE46A00DFFE62 /* StoreSubscriptionTests.swift */; };
73F39F3A1D3EE46A00DFFE62 /* StoreSubscriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73F39F371D3EE46A00DFFE62 /* StoreSubscriptionTests.swift */; };
759801931FB16D2D006EDE17 /* AutomaticallySkipRepeatsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 759801921FB16D2D006EDE17 /* AutomaticallySkipRepeatsTests.swift */; };
81BCBECE1C63167A00AA4F03 /* Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81BCBECD1C63167A00AA4F03 /* Subscription.swift */; };
81BCBECF1C63167A00AA4F03 /* Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81BCBECD1C63167A00AA4F03 /* Subscription.swift */; };
81BCBED01C63167A00AA4F03 /* Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81BCBECD1C63167A00AA4F03 /* Subscription.swift */; };
Expand Down Expand Up @@ -160,6 +161,7 @@
73E545D91D22BCD600D114E8 /* XCTest+Assertions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "XCTest+Assertions.swift"; sourceTree = "<group>"; };
73F39F331D3EE3C300DFFE62 /* StoreDispatchTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoreDispatchTests.swift; sourceTree = "<group>"; };
73F39F371D3EE46A00DFFE62 /* StoreSubscriptionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoreSubscriptionTests.swift; sourceTree = "<group>"; };
759801921FB16D2D006EDE17 /* AutomaticallySkipRepeatsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutomaticallySkipRepeatsTests.swift; sourceTree = "<group>"; };
81BCBECD1C63167A00AA4F03 /* Subscription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Subscription.swift; sourceTree = "<group>"; };
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>"; };
B5C08F1806830A13C9006A27 /* libPods-SwiftLintIntegration.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-SwiftLintIntegration.a"; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -301,6 +303,7 @@
621C068B1C278BEF008029AE /* TypeHelperTests.swift */,
73E545D91D22BCD600D114E8 /* XCTest+Assertions.swift */,
73723E031D30AEF3006139F0 /* XCTest+Compatibility.swift */,
759801921FB16D2D006EDE17 /* AutomaticallySkipRepeatsTests.swift */,
);
path = ReSwiftTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -854,6 +857,7 @@
73F39F381D3EE46A00DFFE62 /* StoreSubscriptionTests.swift in Sources */,
621C068C1C278BEF008029AE /* TypeHelperTests.swift in Sources */,
259737EA1C2C611600869B8F /* StoreMiddlewareTests.swift in Sources */,
759801931FB16D2D006EDE17 /* AutomaticallySkipRepeatsTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
51 changes: 37 additions & 14 deletions ReSwift/CoreTypes/Store.swift
Expand Up @@ -67,27 +67,17 @@ open class Store<State: StateType>: StoreType {
}
}

open func subscribe<S: StoreSubscriber>(_ subscriber: S)
where S.StoreSubscriberStateType == State {
_ = subscribe(subscriber, transform: nil)
}

open func subscribe<SelectedState, S: StoreSubscriber>(
_ subscriber: S, transform: ((Subscription<State>) -> Subscription<SelectedState>)?
) where S.StoreSubscriberStateType == SelectedState
fileprivate func _subscribe<SelectedState, S: StoreSubscriber>(
_ subscriber: S, originalSubscription: Subscription<State>,
transformedSubscription: Subscription<SelectedState>?)
where S.StoreSubscriberStateType == SelectedState
{
// If the same subscriber is already registered with the store, replace the existing
// subscription with the new one.
if let index = subscriptions.index(where: { $0.subscriber === subscriber }) {
subscriptions.remove(at: index)
}

// Create a subscription for the new subscriber.
let originalSubscription = Subscription<State>()
// Call the optional transformation closure. This allows callers to modify
// the subscription, e.g. in order to subselect parts of the store's state.
let transformedSubscription = transform?(originalSubscription)

let subscriptionBox = self.subscriptionBox(
originalSubscription: originalSubscription,
transformedSubscription: transformedSubscription,
Expand All @@ -101,6 +91,25 @@ open class Store<State: StateType>: StoreType {
}
}

open func subscribe<S: StoreSubscriber>(_ subscriber: S)
where S.StoreSubscriberStateType == State {
_ = subscribe(subscriber, transform: nil)
}

open func subscribe<SelectedState, S: StoreSubscriber>(
_ subscriber: S, transform: ((Subscription<State>) -> Subscription<SelectedState>)?
) where S.StoreSubscriberStateType == SelectedState
{
// Create a subscription for the new subscriber.
let originalSubscription = Subscription<State>()
// Call the optional transformation closure. This allows callers to modify
// the subscription, e.g. in order to subselect parts of the store's state.
let transformedSubscription = transform?(originalSubscription)

_subscribe(subscriber, originalSubscription: originalSubscription,
transformedSubscription: transformedSubscription)
}

internal func subscriptionBox<T>(
originalSubscription: Subscription<State>,
transformedSubscription: Subscription<T>?,
Expand Down Expand Up @@ -182,4 +191,18 @@ extension Store where State: Equatable {
where S.StoreSubscriberStateType == State {
_ = subscribe(subscriber, transform: { $0.skipRepeats() })
}

open func subscribe<SelectedState: Equatable, S: StoreSubscriber>(
_ subscriber: S, transform: ((Subscription<State>) -> Subscription<SelectedState>)?
) where S.StoreSubscriberStateType == SelectedState
{
let originalSubscription = Subscription<State>()

var transformedSubscription = transform?(originalSubscription)
transformedSubscription = transformedSubscription?.skipRepeats()

_subscribe(subscriber,
originalSubscription: originalSubscription,
transformedSubscription: transformedSubscription)
}
}
82 changes: 82 additions & 0 deletions ReSwiftTests/AutomaticallySkipRepeatsTests.swift
@@ -0,0 +1,82 @@
//
// AutomaticallySkipRepeatsTests.swift
// ReSwift
//
// Created by Daniel Martín Prieto on 03/11/2017.
// Copyright © 2017 Benjamin Encz. All rights reserved.
//
import XCTest
import ReSwift

class AutomaticallySkipRepeatsTests: XCTestCase {

private var store: Store<State>!
fileprivate var subscriptionUpdates: Int = 0

override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
store = Store<State>(reducer: reducer, state: nil)
subscriptionUpdates = 0
}

override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
store = nil
subscriptionUpdates = 0
super.tearDown()
}

func testInitialSubscription() {
store.subscribe(self) { $0.select { $0.name } }
XCTAssertEqual(self.subscriptionUpdates, 1)
}

func testDispatchUnrelatedActionWithExplicitSkipRepeats() {
store.subscribe(self) { $0.select { $0.name }.skipRepeats() }
XCTAssertEqual(self.subscriptionUpdates, 1)
store.dispatch(ChangeAge(newAge: 30))
XCTAssertEqual(self.subscriptionUpdates, 1)
}

func testDispatchUnrelatedActionWithoutExplicitSkipRepeats() {
store.subscribe(self) { $0.select { $0.name } }
XCTAssertEqual(self.subscriptionUpdates, 1)
store.dispatch(ChangeAge(newAge: 30))
XCTAssertEqual(self.subscriptionUpdates, 1)
}

}

extension AutomaticallySkipRepeatsTests: StoreSubscriber {
func newState(state: String) {
subscriptionUpdates += 1
}
}

private struct State: StateType {
let age: Int
let name: String
}

extension State: Equatable {
static func == (lhs: State, rhs: State) -> Bool {
return lhs.age == rhs.age && lhs.name == rhs.name
}
}

struct ChangeAge: Action {
let newAge: Int
}

private let initialState = State(age: 29, name: "Daniel")

private func reducer(action: Action, state: State?) -> State {
let defaultState = state ?? initialState
switch action {
case let changeAge as ChangeAge:
return State(age: changeAge.newAge, name: defaultState.name)
default:
return defaultState
}
}
22 changes: 9 additions & 13 deletions ReSwiftTests/StoreSubscriberTests.swift
Expand Up @@ -124,31 +124,28 @@ class StoreSubscriberTests: XCTestCase {
XCTAssertEqual(subscriber.newStateCallCount, 1)
}

/**
it skips repeated state values by default when the selected substate is `Equatable`.
*/
func testSkipsStateUpdatesForEquatableSubstatesByDefault() {
let reducer = TestValueStringReducer()
let state = TestStringAppState()
func testPassesOnDuplicateSubstateUpdatesByDefault() {
let reducer = TestNonEquatableReducer()
let state = TestNonEquatable()
let store = Store(reducer: reducer.handleAction, state: state)
let subscriber = TestFilteredSubscriber<String>()
let subscriber = TestFilteredSubscriber<NonEquatable>()

store.subscribe(subscriber) {
$0.select { $0.testValue }
}

XCTAssertEqual(subscriber.receivedValue, "Initial")
XCTAssertEqual(subscriber.receivedValue.testValue, "Initial")

store.dispatch(SetValueStringAction("Initial"))
store.dispatch(SetNonEquatableAction(NonEquatable()))

XCTAssertEqual(subscriber.receivedValue, "Initial")
XCTAssertEqual(subscriber.newStateCallCount, 1)
XCTAssertEqual(subscriber.receivedValue.testValue, "Initial")
XCTAssertEqual(subscriber.newStateCallCount, 2)
}

func testSkipsStateUpdatesForEquatableStateByDefault() {
let reducer = TestValueStringReducer()
let state = TestStringAppState()
let store = Store(reducer: reducer.handleAction, state: state)
let store = Store(reducer: reducer.handleAction, state: state, middleware: [])
let subscriber = TestFilteredSubscriber<TestStringAppState>()

store.subscribe(subscriber)
Expand All @@ -160,7 +157,6 @@ class StoreSubscriberTests: XCTestCase {
XCTAssertEqual(subscriber.receivedValue.testValue, "Initial")
XCTAssertEqual(subscriber.newStateCallCount, 1)
}

}

class TestFilteredSubscriber<T>: StoreSubscriber {
Expand Down
40 changes: 40 additions & 0 deletions ReSwiftTests/TestFakes.swift
Expand Up @@ -31,6 +31,22 @@ extension TestStringAppState: Equatable {
}
}

struct TestNonEquatable: StateType {
var testValue: NonEquatable

init() {
testValue = NonEquatable()
}
}

struct NonEquatable {
var testValue: String

init() {
testValue = "Initial"
}
}

struct TestCustomAppState: StateType {
var substate: TestCustomSubstate

Expand Down Expand Up @@ -108,6 +124,15 @@ struct SetCustomSubstateAction: StandardActionConvertible {
}
}

struct SetNonEquatableAction: Action {
var value: NonEquatable
static let type = "SetNonEquatableAction"

init (_ value: NonEquatable) {
self.value = value
}
}

struct TestReducer {
func handleAction(action: Action, state: TestAppState?) -> TestAppState {
var state = state ?? TestAppState()
Expand Down Expand Up @@ -150,6 +175,21 @@ struct TestCustomAppStateReducer {
}
}

struct TestNonEquatableReducer {
func handleAction(action: Action, state: TestNonEquatable?) ->
TestNonEquatable {
var state = state ?? TestNonEquatable()

switch action {
case let action as SetNonEquatableAction:
state.testValue = action.value
return state
default:
return state
}
}
}

class TestStoreSubscriber<T>: StoreSubscriber {
var receivedStates: [T] = []

Expand Down

0 comments on commit d4cb424

Please sign in to comment.