Skip to content

Commit b76faa0

Browse files
authored
Merge pull request #3 from ReSwift/feature/1.1
Add binding functions and change state lensing to use functions
2 parents a76d7a2 + 7f8ea98 commit b76faa0

File tree

7 files changed

+163
-37
lines changed

7 files changed

+163
-37
lines changed
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
public enum ActionStrata<Raw, Refined> {
1+
public enum ActionStrata<RawAction, RefinedAction> {
2+
public typealias Raw = RawAction
3+
public typealias Refined = RefinedAction
24
case raw(Raw)
35
case refined(Refined)
46
}

Sources/RecombinePackage/Middleware.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import Combine
22

3-
/// Middleware is a structure that allows you to modify, filter out and create more
4-
/// actions, before the action being handled reaches the store.
3+
/// Middleware is a dependency injection structure that allows you to transform raw actions into refined ones,
4+
/// Refined actions produced by Middleware are then forwarded to the main reducer.
5+
///
56
public struct Middleware<State, Input, Output> {
67
public typealias StatePublisher = Publishers.First<Published<State>.Publisher>
78
public typealias Function = (StatePublisher, Input) -> AnyPublisher<Output, Never>
89
public typealias Transform<Result> = (StatePublisher, Output) -> Result
910
internal let transform: Function
1011

11-
/// Create a blank slate Middleware.
12+
/// Create a passthrough Middleware.
1213
public init() where Input == Output {
1314
self.transform = { Just($1).eraseToAnyPublisher() }
1415
}

Sources/RecombinePackage/Store/AnyStore.swift

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Combine
22

33
public class AnyStore<BaseState, SubState, RawAction, BaseRefinedAction, SubRefinedAction>: StoreProtocol {
44
public let underlying: BaseStore<BaseState, RawAction, BaseRefinedAction>
5-
public let keyPath: KeyPath<BaseState, SubState>
5+
public let stateLens: (BaseState) -> SubState
66
public let actionPromotion: (SubRefinedAction) -> BaseRefinedAction
77
private var cancellables = Set<AnyCancellable>()
88
@Published
@@ -17,7 +17,7 @@ public class AnyStore<BaseState, SubState, RawAction, BaseRefinedAction, SubRefi
1717
Store.SubRefinedAction == SubRefinedAction
1818
{
1919
underlying = store.underlying
20-
keyPath = store.keyPath
20+
stateLens = store.stateLens
2121
actionPromotion = store.actionPromotion
2222
self.state = store.state
2323
store.statePublisher.sink { [unowned self] state in
@@ -27,7 +27,7 @@ public class AnyStore<BaseState, SubState, RawAction, BaseRefinedAction, SubRefi
2727
}
2828

2929
public func lensing<NewState, NewAction>(
30-
state keyPath: KeyPath<SubState, NewState>,
30+
state lens: @escaping (SubState) -> NewState,
3131
actions transform: @escaping (NewAction) -> SubRefinedAction
3232
) -> LensedStore<
3333
BaseState,
@@ -36,9 +36,10 @@ public class AnyStore<BaseState, SubState, RawAction, BaseRefinedAction, SubRefi
3636
BaseRefinedAction,
3737
NewAction
3838
> {
39-
.init(
39+
let stateLens = self.stateLens
40+
return .init(
4041
store: underlying,
41-
lensing: self.keyPath.appending(path: keyPath),
42+
lensing: { lens(stateLens($0)) },
4243
actionPromotion: { self.actionPromotion(transform($0)) }
4344
)
4445
}

Sources/RecombinePackage/Store/BaseStore.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public class BaseStore<State, RawAction, RefinedAction>: StoreProtocol {
88
public private(set) var state: State
99
public var statePublisher: Published<State>.Publisher { $state }
1010
public var underlying: BaseStore<State, RawAction, RefinedAction> { self }
11-
public let keyPath: KeyPath<State, State> = \.self
11+
public let stateLens: (State) -> State = { $0 }
1212
public let rawActions = PassthroughSubject<RawAction, Never>()
1313
public let refinedActions = PassthroughSubject<RefinedAction, Never>()
1414
public let actionPromotion: (RefinedAction) -> RefinedAction = { $0 }
@@ -44,6 +44,7 @@ public class BaseStore<State, RawAction, RefinedAction>: StoreProtocol {
4444
action: action
4545
)
4646
}
47+
.removeDuplicates(by: stateEquality)
4748
.receive(on: scheduler)
4849
.sink { [unowned self] state in
4950
self.state = state
@@ -67,7 +68,7 @@ public class BaseStore<State, RawAction, RefinedAction>: StoreProtocol {
6768
}
6869

6970
public func lensing<NewState, NewAction>(
70-
state keyPath: KeyPath<SubState, NewState>,
71+
state lens: @escaping (SubState) -> NewState,
7172
actions transform: @escaping (NewAction) -> SubRefinedAction
7273
) -> LensedStore<
7374
State,
@@ -76,7 +77,7 @@ public class BaseStore<State, RawAction, RefinedAction>: StoreProtocol {
7677
RefinedAction,
7778
NewAction
7879
> {
79-
.init(store: self, lensing: keyPath, actionPromotion: transform)
80+
.init(store: self, lensing: lens, actionPromotion: transform)
8081
}
8182

8283
open func dispatch<S: Sequence>(refined actions: S) where S.Element == RefinedAction {

Sources/RecombinePackage/Store/LensedStore.swift

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,18 @@ public class LensedStore<BaseState, SubState: Equatable, RawAction, BaseRefinedA
66
public private(set) var state: SubState
77
public var statePublisher: Published<SubState>.Publisher { $state }
88
public let underlying: BaseStore<BaseState, RawAction, BaseRefinedAction>
9-
public let keyPath: KeyPath<BaseState, SubState>
9+
public let stateLens: (BaseState) -> SubState
1010
public let actions = PassthroughSubject<SubRefinedAction, Never>()
1111
public let actionPromotion: (SubRefinedAction) -> BaseRefinedAction
1212
private var cancellables = Set<AnyCancellable>()
1313

14-
public required init(store: StoreType, lensing keyPath: KeyPath<BaseState, SubState>, actionPromotion: @escaping (SubRefinedAction) -> BaseRefinedAction) {
14+
public required init(store: StoreType, lensing lens: @escaping (BaseState) -> SubState, actionPromotion: @escaping (SubRefinedAction) -> BaseRefinedAction) {
1515
self.underlying = store
16-
self.keyPath = keyPath
16+
self.stateLens = lens
1717
self.actionPromotion = actionPromotion
18-
state = store.state[keyPath: keyPath]
18+
state = lens(store.state)
1919
store.$state
20-
.map { $0[keyPath: keyPath] }
20+
.map(lens)
2121
.removeDuplicates()
2222
.sink { [unowned self] state in
2323
self.state = state
@@ -31,7 +31,7 @@ public class LensedStore<BaseState, SubState: Equatable, RawAction, BaseRefinedA
3131
}
3232

3333
public func lensing<NewState, NewAction>(
34-
state keyPath: KeyPath<SubState, NewState>,
34+
state lens: @escaping (SubState) -> NewState,
3535
actions transform: @escaping (NewAction) -> SubRefinedAction
3636
) -> LensedStore<
3737
BaseState,
@@ -40,9 +40,10 @@ public class LensedStore<BaseState, SubState: Equatable, RawAction, BaseRefinedA
4040
BaseRefinedAction,
4141
NewAction
4242
> {
43-
.init(
43+
let stateLens = self.stateLens
44+
return .init(
4445
store: underlying,
45-
lensing: self.keyPath.appending(path: keyPath),
46+
lensing: { lens(stateLens($0)) },
4647
actionPromotion: { self.actionPromotion(transform($0)) }
4748
)
4849
}
@@ -56,8 +57,8 @@ public class LensedStore<BaseState, SubState: Equatable, RawAction, BaseRefinedA
5657
}
5758
}
5859

59-
extension LensedStore where BaseRefinedAction == SubRefinedAction {
60-
convenience init(store: StoreType, lensing keyPath: KeyPath<BaseState, SubState>) {
61-
self.init(store: store, lensing: keyPath, actionPromotion: { $0 })
60+
public extension LensedStore where BaseRefinedAction == SubRefinedAction {
61+
convenience init(store: StoreType, lensing lens: @escaping (BaseState) -> SubState) {
62+
self.init(store: store, lensing: lens, actionPromotion: { $0 })
6263
}
6364
}

Sources/RecombinePackage/Store/StoreProtocol.swift

Lines changed: 93 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Combine
2+
import SwiftUI
23

34
public protocol StoreProtocol: ObservableObject, Subscriber {
45
associatedtype BaseState
@@ -9,12 +10,12 @@ public protocol StoreProtocol: ObservableObject, Subscriber {
910
var state: SubState { get }
1011
var statePublisher: Published<SubState>.Publisher { get }
1112
var underlying: BaseStore<BaseState, RawAction, BaseRefinedAction> { get }
12-
var keyPath: KeyPath<BaseState, SubState> { get }
13+
var stateLens: (BaseState) -> SubState { get }
1314
var actionPromotion: (SubRefinedAction) -> BaseRefinedAction { get }
1415
func dispatch<S: Sequence>(raw: S) where S.Element == RawAction
1516
func dispatch<S: Sequence>(refined: S) where S.Element == SubRefinedAction
1617
func lensing<NewState, NewAction>(
17-
state keyPath: KeyPath<SubState, NewState>,
18+
state lens: @escaping (SubState) -> NewState,
1819
actions transform: @escaping (NewAction) -> SubRefinedAction
1920
) -> LensedStore<
2021
BaseState,
@@ -27,30 +28,109 @@ public protocol StoreProtocol: ObservableObject, Subscriber {
2728
}
2829

2930
public extension StoreProtocol {
30-
func lensing<NewState, NewAction>(
31-
actions transform: @escaping (NewAction) -> SubRefinedAction
31+
func lensing<NewState>(
32+
state lens: @escaping (SubState) -> NewState
3233
) -> LensedStore<
3334
BaseState,
3435
NewState,
3536
RawAction,
3637
BaseRefinedAction,
38+
SubRefinedAction
39+
> {
40+
lensing(state: lens, actions: { $0 })
41+
}
42+
43+
func lensing<NewState>(
44+
state keyPath: KeyPath<SubState, NewState>
45+
) -> LensedStore<
46+
BaseState,
47+
NewState,
48+
RawAction,
49+
BaseRefinedAction,
50+
SubRefinedAction
51+
> {
52+
lensing(state: { $0[keyPath: keyPath] })
53+
}
54+
55+
func lensing<NewAction>(
56+
actions transform: @escaping (NewAction) -> SubRefinedAction
57+
) -> LensedStore<
58+
BaseState,
59+
SubState,
60+
RawAction,
61+
BaseRefinedAction,
3762
NewAction
38-
> where NewState == SubState {
39-
lensing(state: \.self, actions: transform)
63+
> {
64+
lensing(state: { $0 }, actions: transform)
4065
}
4166

4267
func lensing<NewState, NewAction>(
43-
state keyPath: KeyPath<SubState, NewState>
68+
state keyPath: KeyPath<SubState, NewState>,
69+
actions transform: @escaping (NewAction) -> SubRefinedAction
4470
) -> LensedStore<
4571
BaseState,
4672
NewState,
4773
RawAction,
4874
BaseRefinedAction,
4975
NewAction
50-
> where NewAction == SubRefinedAction {
51-
lensing(state: keyPath, actions: { $0 })
76+
> {
77+
lensing(state: { $0[keyPath: keyPath] }, actions: transform)
5278
}
79+
}
5380

81+
public extension StoreProtocol {
82+
/// Create a SwiftUI Binding from a lensing function and a `SubRefinedAction`.
83+
/// - Parameters:
84+
/// - lens: A lens to the state property.
85+
/// - action: The refined action which will be called when the value is changed.
86+
/// - Returns: A `Binding` whose getter is the property and whose setter dispatches the refined action.
87+
func binding<Value>(
88+
state lens: @escaping (SubState) -> Value,
89+
actions transform: @escaping (Value) -> SubRefinedAction
90+
) -> Binding<Value> {
91+
.init(
92+
get: { lens(self.state) },
93+
set: { self.dispatch(refined: transform($0)) }
94+
)
95+
}
96+
97+
/// Create a SwiftUI Binding from the `SubState` of the store and a `SubRefinedAction`.
98+
/// - Parameters:
99+
/// - actions: The refined action which will be called when the value is changed.
100+
/// - Returns: A `Binding` whose getter is the state and whose setter dispatches the refined action.
101+
func binding(
102+
actions transform: @escaping (SubState) -> SubRefinedAction
103+
) -> Binding<SubState> {
104+
.init(
105+
get: { self.state },
106+
set: { self.dispatch(refined: transform($0)) }
107+
)
108+
}
109+
110+
/// Create a SwiftUI Binding from a lensing function when the value of that function is equivalent to `SubRefinedAction`.
111+
/// - Parameters:
112+
/// - lens: A lens to the state property.
113+
/// - Returns: A `Binding` whose getter is the property and whose setter dispatches the store's refined action.
114+
func binding<Value>(
115+
state lens: @escaping (SubState) -> Value
116+
) -> Binding<Value> where SubRefinedAction == Value {
117+
.init(
118+
get: { lens(self.state) },
119+
set: { self.dispatch(refined: $0) }
120+
)
121+
}
122+
123+
/// Create a SwiftUI Binding from the `SubState` when its value is equivalent to `SubRefinedAction`.
124+
/// - Returns: A `Binding` whose getter is the state and whose setter dispatches the store's refined action.
125+
func binding() -> Binding<SubState> where SubRefinedAction == SubState {
126+
.init(
127+
get: { self.state },
128+
set: { self.dispatch(refined: $0) }
129+
)
130+
}
131+
}
132+
133+
public extension StoreProtocol {
54134
func dispatch(refined actions: SubRefinedAction...) {
55135
dispatch(refined: actions)
56136
}
@@ -64,12 +144,12 @@ public extension StoreProtocol {
64144
}
65145
}
66146

67-
extension StoreProtocol {
68-
public func receive(subscription: Subscription) {
147+
public extension StoreProtocol {
148+
func receive(subscription: Subscription) {
69149
subscription.request(.unlimited)
70150
}
71151

72-
public func receive(_ input: ActionStrata<RawAction, SubRefinedAction>) -> Subscribers.Demand {
152+
func receive(_ input: ActionStrata<RawAction, SubRefinedAction>) -> Subscribers.Demand {
73153
switch input {
74154
case let .raw(action):
75155
dispatch(raw: action)
@@ -79,5 +159,5 @@ extension StoreProtocol {
79159
return .unlimited
80160
}
81161

82-
public func receive(completion: Subscribers.Completion<Never>) {}
162+
func receive(completion: Subscribers.Completion<Never>) {}
83163
}

Tests/RecombineTests/StoreTests.swift

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ class StoreTests: XCTestCase {
3030
middleware: .init(),
3131
publishOn: ImmediateScheduler.shared
3232
)
33-
let subStore = store.lensing(state: \.subState.value, actions: TestFakes.NestedTest.Action.sub)
33+
let subStore = store.lensing(
34+
state: \.subState.value,
35+
actions: TestFakes.NestedTest.Action.sub
36+
)
3437
let stateRecorder = subStore.$state.dropFirst().record()
3538
let actionsRecorder = subStore.actions.record()
3639

@@ -43,6 +46,43 @@ class StoreTests: XCTestCase {
4346
let actions = try wait(for: actionsRecorder.prefix(1), timeout: 1)
4447
XCTAssertEqual(actions, [.set(string)])
4548
}
49+
50+
func testBinding() throws {
51+
let store = BaseStore(
52+
state: TestFakes.NestedTest.State(),
53+
reducer: TestFakes.NestedTest.reducer,
54+
middleware: .init(),
55+
publishOn: ImmediateScheduler.shared
56+
)
57+
let binding1 = store.binding(
58+
state: \.subState.value,
59+
actions: { .sub(.set("\($0)1")) }
60+
)
61+
let binding2 = store.lensing(
62+
state: \.subState.value
63+
).binding(
64+
actions: { .sub(.set("\($0)2")) }
65+
)
66+
let binding3 = store.lensing(
67+
state: \.subState,
68+
actions: { .sub(.set("\($0)3")) }
69+
).binding(
70+
state: \.value
71+
)
72+
let stateRecorder = store.$state.dropFirst().record()
73+
74+
let string = "Oh Yeah!"
75+
76+
binding1.wrappedValue = string
77+
binding2.wrappedValue = string
78+
binding3.wrappedValue = string
79+
80+
let state = try wait(for: stateRecorder.prefix(3), timeout: 1)
81+
XCTAssertEqual(
82+
state.map(\.subState.value),
83+
zip(repeatElement(string, count: 3), 1...).map { "\($0)\($1)" }
84+
)
85+
}
4686
}
4787

4888
// Used for deinitialization test

0 commit comments

Comments
 (0)