Skip to content

Commit

Permalink
Allow to notify on changes only
Browse files Browse the repository at this point in the history
  • Loading branch information
luizmb committed Oct 20, 2019
1 parent 6cd4f39 commit 3c4f9cc
Show file tree
Hide file tree
Showing 13 changed files with 327 additions and 68 deletions.
1 change: 0 additions & 1 deletion Sources/CombineRex/BindableStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ extension BindableStore where ViewState: Equatable {
self.init(initialState: initialState,
viewStore: viewStore,
removeDuplicates: { $0.removeDuplicates() })

}
}

Expand Down
17 changes: 0 additions & 17 deletions Sources/SwiftRex/CoreTypes/Pipeline/Middleware.swift
Original file line number Diff line number Diff line change
Expand Up @@ -171,23 +171,6 @@ public protocol Middleware: class {
func handle(action: InputActionType, next: @escaping Next)
}

extension Middleware {
/**
Handles the incoming actions and may or not start async tasks, check the latest state at any point or dispatch
additional actions. This is also a good place for analytics, tracking, logging and telemetry.
- Parameters:
- action: the action to be handled
- next: opportunity to call the next middleware in the chain and, eventually, the reducer pipeline. Call it
only once, not more or less than once. Call it from the same thread and runloop where the handle function
is executed, never from a completion handler or dispatch queue block. In case you don't need to compare
state before and after it's changed from the reducers, please consider to add a `defer` block with `next()`
on it, at the beginning of `handle` function.
*/
public func handle(action: InputActionType, next: @escaping Next) {
next()
}
}

// sourcery: AutoMockable
// sourcery: AutoMockableGeneric = StateType
// sourcery: AutoMockableGeneric = OutputActionType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,20 @@ public struct ReduxPipelineWrapper<MiddlewareType: Middleware>: ActionHandler

public init(state: UnfailableReplayLastSubjectType<StateType>,
reducer: Reducer<ActionType, StateType>,
middleware: MiddlewareType) {
middleware: MiddlewareType,
emitsChange: @escaping (StateType, StateType) -> Bool) {
self.middleware = middleware

let reduce: (ActionType) -> Void = { action in
state.mutate { value in
value = reducer.reduce(action, value)
}
state.mutate(
when: { $0 },
action: { value -> Bool in
let newValue = reducer.reduce(action, value)
guard emitsChange(value, newValue) else { return false }
value = newValue
return true
}
)
}

let middlewarePipeline: (ActionType) -> Void = { [unowned middleware] action in
Expand All @@ -42,3 +49,11 @@ public struct ReduxPipelineWrapper<MiddlewareType: Middleware>: ActionHandler
onAction(action)
}
}

extension ReduxPipelineWrapper where StateType: Equatable {
public init(state: UnfailableReplayLastSubjectType<StateType>,
reducer: Reducer<ActionType, StateType>,
middleware: MiddlewareType) {
self.init(state: state, reducer: reducer, middleware: middleware, emitsChange: !=)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,30 @@ open class ReduxStoreBase<ActionType, StateType>: ReduxStoreProtocol {
*/
public init<M: Middleware>(subject: UnfailableReplayLastSubjectType<StateType>,
reducer: Reducer<ActionType, StateType>,
middleware: M)
middleware: M,
emitsChange: ShouldEmitChange<StateType> = .always)
where M.InputActionType == ActionType, M.InputActionType == M.OutputActionType, M.StateType == StateType {
self.subject = subject
self.pipeline = .init(state: subject, reducer: reducer, middleware: AnyMiddleware(middleware))
self.pipeline = .init(state: subject, reducer: reducer, middleware: AnyMiddleware(middleware), emitsChange: emitsChange.evaluate)
}
}

public struct ShouldEmitChange<StateType> {
fileprivate let evaluate: (StateType, StateType) -> Bool

private init(evaluate: @escaping (StateType, StateType) -> Bool) {
self.evaluate = evaluate
}
}

extension ShouldEmitChange {
public static var always: ShouldEmitChange<StateType> { .init(evaluate: { _, _ in true }) }
public static var never: ShouldEmitChange<StateType> { .init(evaluate: { _, _ in false }) }
public static var when: (@escaping (StateType, StateType) -> Bool) -> ShouldEmitChange<StateType> {
ShouldEmitChange<StateType>.init
}
}

extension ShouldEmitChange where StateType: Equatable {
public static var whenChange: ShouldEmitChange<StateType> { .init(evaluate: !=) }
}
9 changes: 9 additions & 0 deletions Sources/SwiftRex/Foundation/ReactiveWrappers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,15 @@ extension ReplayLastSubjectType {
subscriber.onValue(currentValue)
return result
}

@discardableResult
public func mutate<Result>(when condition: @escaping (Result) -> Bool, action: (inout Element) -> Result) -> Result {
var currentValue = value()
let result = action(&currentValue)
guard condition(result) else { return result }
subscriber.onValue(currentValue)
return result
}
}

public typealias UnfailableReplayLastSubjectType<Element> = ReplayLastSubjectType<Element, Never>
16 changes: 16 additions & 0 deletions Sources/SwiftRex/Middlewares/IdentityMiddleware.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,20 @@ public final class IdentityMiddleware<InputActionType, OutputActionType, GlobalS
fatalError("No context set for middleware PipelineMiddleware, please be sure to configure your middleware prior to usage")
}
}

/**
Handles the incoming actions and may or not start async tasks, check the latest state at any point or dispatch
additional actions. This is also a good place for analytics, tracking, logging and telemetry.
In this empty implementation, will do nothing but call next delegate.
- Parameters:
- action: the action to be handled
- next: opportunity to call the next middleware in the chain and, eventually, the reducer pipeline. Call it
only once, not more or less than once. Call it from the same thread and runloop where the handle function
is executed, never from a completion handler or dispatch queue block. In case you don't need to compare
state before and after it's changed from the reducers, please consider to add a `defer` block with `next()`
on it, at the beginning of `handle` function.
*/
public func handle(action: InputActionType, next: @escaping Next) {
next()
}
}
4 changes: 4 additions & 0 deletions SwiftRex.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@
D598492E22C023CD00F99D09 /* ReplayLastSubjectType+BehaviorSubject.swift in Sources */ = {isa = PBXBuildFile; fileRef = D51A223422BFF1EA00194408 /* ReplayLastSubjectType+BehaviorSubject.swift */; };
D598492F22C023CE00F99D09 /* ReplayLastSubjectType+BehaviorSubject.swift in Sources */ = {isa = PBXBuildFile; fileRef = D51A223422BFF1EA00194408 /* ReplayLastSubjectType+BehaviorSubject.swift */; };
D598493022C023CF00F99D09 /* ReplayLastSubjectType+BehaviorSubject.swift in Sources */ = {isa = PBXBuildFile; fileRef = D51A223422BFF1EA00194408 /* ReplayLastSubjectType+BehaviorSubject.swift */; };
D59DDF7F235CC8980056A10E /* IntegrationCounterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59DDF7E235CC8980056A10E /* IntegrationCounterTests.swift */; };
D5A022B022BED5C600797438 /* SubscriberTypeBridgeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A022AF22BED5C600797438 /* SubscriberTypeBridgeTests.swift */; };
D5A022BA22BED8B000797438 /* TypeErase.generated.abstract.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55EFB0C207EBAA300BC8822 /* TypeErase.generated.abstract.swift */; };
D5A022BB22BED8B000797438 /* TypeErase.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59D57EB207AA1B40010412E /* TypeErase.generated.swift */; };
Expand Down Expand Up @@ -526,6 +527,7 @@
D59D5807207AA1B40010412E /* StateProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StateProvider.swift; sourceTree = "<group>"; };
D59D5809207AA1B40010412E /* StoreType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoreType.swift; sourceTree = "<group>"; };
D59D580A207AA1B40010412E /* ReduxStoreBase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReduxStoreBase.swift; sourceTree = "<group>"; };
D59DDF7E235CC8980056A10E /* IntegrationCounterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrationCounterTests.swift; sourceTree = "<group>"; };
D5A022AF22BED5C600797438 /* SubscriberTypeBridgeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriberTypeBridgeTests.swift; sourceTree = "<group>"; };
D5A2EAE922B9A00E00CA10B8 /* ReactiveSwiftRex.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ReactiveSwiftRex.h; sourceTree = "<group>"; };
D5A2EAEE22B9A05600CA10B8 /* RxSwiftRex.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RxSwiftRex.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -888,6 +890,7 @@
children = (
D5326483233E30F800CF0589 /* IssueTrackerTests39.swift */,
D56E317D235A746C00BEF634 /* IntegrationWithComposableMiddlewareTests.swift */,
D59DDF7E235CC8980056A10E /* IntegrationCounterTests.swift */,
);
path = Integration;
sourceTree = "<group>";
Expand Down Expand Up @@ -2965,6 +2968,7 @@
D5FEA881234D7DCF003486A7 /* BlockPublisher.swift in Sources */,
D5FEA883234D7EBC003486A7 /* SubscriberTypeBridgeTests.swift in Sources */,
D56E317C235A5D7600BEF634 /* IssueTrackerTests39.swift in Sources */,
D59DDF7F235CC8980056A10E /* IntegrationCounterTests.swift in Sources */,
D5FEA885234D7F1D003486A7 /* ReplayLastSubjectTypeBridgeTests.swift in Sources */,
D56E317E235A746C00BEF634 /* IntegrationWithComposableMiddlewareTests.swift in Sources */,
);
Expand Down
21 changes: 6 additions & 15 deletions SwiftRex.xcworkspace/xcshareddata/xcschemes/Playgrounds.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D532CA05223EC2C900AB13EF"
BuildableName = "RxSwiftRex.framework"
BlueprintName = "SwiftRex iOS RxSwift"
BlueprintIdentifier = "D5C1C5F722BFE6AA00532BCF"
BuildableName = "CombineRex.framework"
BlueprintName = "SwiftRex iOS Combine"
ReferencedContainer = "container:SwiftRex.xcodeproj">
</BuildableReference>
</BuildActionEntry>
Expand All @@ -40,15 +40,6 @@
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D532CA05223EC2C900AB13EF"
BuildableName = "RxSwiftRex.framework"
BlueprintName = "SwiftRex iOS RxSwift"
ReferencedContainer = "container:SwiftRex.xcodeproj">
</BuildableReference>
</MacroExpansion>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
Expand All @@ -59,9 +50,9 @@
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D532CA05223EC2C900AB13EF"
BuildableName = "RxSwiftRex.framework"
BlueprintName = "SwiftRex iOS RxSwift"
BlueprintIdentifier = "D5C1C5F722BFE6AA00532BCF"
BuildableName = "CombineRex.framework"
BlueprintName = "SwiftRex iOS Combine"
ReferencedContainer = "container:SwiftRex.xcodeproj">
</BuildableReference>
</MacroExpansion>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,53 +3,113 @@
// Please start by selecting target Playgrounds and any iPhone from the device list
// Then build the target and run the playground

import Combine
import CombineRex
import PlaygroundSupport
import SwiftRex

PlaygroundPage.current.needsIndefiniteExecution = true

// App state, shared among all modules of our app
struct GlobalState: Equatable, Codable {
var currentNumber = 0
enum AppAction: Equatable {
case event(CounterEvent)
case action(CounterAction)

enum CounterEvent: Equatable {
case requestIncrease, requestDecrease
}

enum CounterAction: Equatable {
case increase, decrease
}

public var event: CounterEvent? {
get {
guard case let .event(value) = self else { return nil }
return value
}
set {
guard case .event = self, let newValue = newValue else { return }
self = .event(newValue)
}
}

public var action: CounterAction? {
get {
guard case let .action(value) = self else { return nil }
return value
}
set {
guard case .action = self, let newValue = newValue else { return }
self = .action(newValue)
}
}
}

// For simplicity, this enum is both an Event and an Action
// DirectLineMiddleware transfers it directly to the reducers
enum CounterEvent: EventProtocol, ActionProtocol, Equatable {
case increase, decrease
struct AppState: Codable, Equatable {
var currentNumber = 0
}

// Only one Action type to handle, no need for sub-reducers
let reducer = Reducer<GlobalState> { state, action in
guard let counterEvent = action as? CounterEvent else { return state }
enum CounterService {
static let middleware = CounterMiddleware()

var state = state
switch counterEvent {
case .increase: state.currentNumber += 1
case .decrease: state.currentNumber -= 1
static let reducer = Reducer<AppAction.CounterAction, Int> { action, state in
switch action {
case .increase: return state + 1
case .decrease: return state - 1
}
}

return state
class CounterMiddleware: Middleware {
typealias InputActionType = AppAction.CounterEvent
typealias OutputActionType = AppAction.CounterAction
typealias StateType = Int

var context: () -> MiddlewareContext<AppAction.CounterAction, Int> = { fatalError("Not set yet") }

func handle(action: AppAction.CounterEvent, next: @escaping Next) {
next()
switch action {
case .requestIncrease: context().dispatch(.increase)
case .requestDecrease: context().dispatch(.decrease)
}
}
}
}

// Store glues all pieces together
final class Store: StoreBase<GlobalState> {
final class Store: ReduxStoreBase<AppAction, AppState> {
init() {
super.init(initialState: GlobalState(), reducer: reducer, middleware: DirectLineMiddleware())
super.init(
subject: .combine(initialValue: AppState()),
reducer:
CounterService.reducer.lift(
action: \AppAction.action,
state: \AppState.currentNumber
),
middleware:
CounterService.middleware.lift(
actionZoomIn: { $0.event },
actionZoomOut: {
return AppAction.action($0)
},
stateZoomIn: { $0.currentNumber }
),
emitsChange: .whenChange
)
}
}

let store = Store()
store
let subscription = store
.statePublisher
.map { String(data: try! JSONEncoder().encode($0), encoding: .utf8)! }
.subscribe(onNext: { print("New state: \($0)") })

store.dispatch(CounterEvent.increase)
store.dispatch(CounterEvent.increase)
store.dispatch(CounterEvent.increase)
store.dispatch(CounterEvent.decrease)
store.dispatch(CounterEvent.increase)
store.dispatch(CounterEvent.decrease)
store.dispatch(CounterEvent.decrease)
.sink { print("New state: \($0)") }

store.dispatch(.event(.requestIncrease))
store.dispatch(.event(.requestIncrease))
store.dispatch(.event(.requestIncrease))
store.dispatch(.event(.requestDecrease))
store.dispatch(.event(.requestIncrease))
store.dispatch(.event(.requestDecrease))
store.dispatch(.event(.requestDecrease))

//: [Next](@next)
2 changes: 1 addition & 1 deletion SwiftRexPlaygrounds.playground/contents.xcplayground
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<playground version='6.0' target-platform='ios' display-mode='rendered' executeOnSourceChanges='false'>
<playground version='6.0' target-platform='ios' display-mode='rendered'>
<pages>
<page name='Simple Counter'/>
<page name='Mock Async Counter'/>
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
Loading

0 comments on commit 3c4f9cc

Please sign in to comment.