Skip to content

Commit d495951

Browse files
authored
Merge pull request #34 from Ryu0118/feature/dependency
Add Utilitis of Dependencies
2 parents d47149c + 55c2758 commit d495951

File tree

10 files changed

+226
-13
lines changed

10 files changed

+226
-13
lines changed

Sources/SimplexArchitecture/Effect/CombineAction.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public extension CombineAction {
2828
.init(kind: .viewAction(action: action))
2929
}
3030

31+
@_disfavoredOverload
3132
@inlinable
3233
static func action(
3334
_ action: Reducer.ReducerAction

Sources/SimplexArchitecture/Effect/SideEffect.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public struct SideEffect<Reducer: ReducerProtocol>: Sendable {
1717
case concurrentReducerAction([Reducer.ReducerAction])
1818
case serialCombineAction([CombineAction<Reducer>])
1919
case concurrentCombineAction([CombineAction<Reducer>])
20+
case runEffects([SideEffect<Reducer>])
2021
}
2122

2223
let kind: EffectKind
@@ -86,4 +87,9 @@ public extension SideEffect {
8687
static func serial(_ actions: CombineAction<Reducer>...) -> Self {
8788
.init(effectKind: .serialCombineAction(actions))
8889
}
90+
91+
@inlinable
92+
static func runEffects(_ effects: [SideEffect<Reducer>]) -> Self {
93+
.init(effectKind: .runEffects(effects))
94+
}
8995
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import Dependencies
2+
3+
public struct _DependenciesOverrideModifier<Base: ReducerProtocol>: ReducerModifier {
4+
public let base: Base
5+
public let override: (inout DependencyValues) -> Void
6+
7+
@usableFromInline
8+
init(base: Base, override: @escaping (inout DependencyValues) -> Void) {
9+
self.base = base
10+
self.override = override
11+
}
12+
13+
@inlinable
14+
public func reduce(into state: StateContainer<Base.Target>, action: Base.Action) -> SideEffect<Base> {
15+
withDependencies(override) {
16+
base.reduce(into: state, action: action)
17+
}
18+
}
19+
20+
@inlinable
21+
public func reduce(into state: StateContainer<Base.Target>, action: Base.ReducerAction) -> SideEffect<Base> {
22+
withDependencies(override) {
23+
base.reduce(into: state, action: action)
24+
}
25+
}
26+
27+
@inlinable
28+
public func dependency<Value>(
29+
_ keyPath: WritableKeyPath<DependencyValues, Value>,
30+
value: Value
31+
) -> Self {
32+
Self(base: self.base) { values in
33+
values[keyPath: keyPath] = value
34+
override(&values)
35+
}
36+
}
37+
}
38+
39+
public extension ReducerProtocol {
40+
@inlinable
41+
func dependency<Value>(
42+
_ keyPath: WritableKeyPath<DependencyValues, Value>,
43+
value: Value
44+
) -> _DependenciesOverrideModifier<Self> {
45+
.init(base: self) { values in
46+
values[keyPath: keyPath] = value
47+
}
48+
}
49+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import Foundation
2+
import Dependencies
3+
4+
public protocol ReducerModifier<Base> {
5+
associatedtype Base: ReducerProtocol
6+
func reduce(into state: StateContainer<Base.Target>, action: Base.Action) -> SideEffect<Base>
7+
func reduce(into state: StateContainer<Base.Target>, action: Base.ReducerAction) -> SideEffect<Base>
8+
}
9+
10+
public extension ReducerModifier {
11+
@inlinable
12+
func reduce(
13+
into state: StateContainer<Base.Target>,
14+
action: CombineAction<Base>
15+
) -> SideEffect<Base> {
16+
switch action.kind {
17+
case let .viewAction(action):
18+
reduce(into: state, action: action)
19+
20+
case let .reducerAction(action):
21+
reduce(into: state, action: action)
22+
}
23+
}
24+
}

Sources/SimplexArchitecture/Reducer/ReducerProtocol.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ public protocol ReducerProtocol<Target> {
114114
/// - action: A ReducerAction that can change the state of View and ReducerState.
115115
/// - Returns: An `SideEffect` representing the side effects generated by the reducer.
116116
func reduce(into state: StateContainer<Target>, action: ReducerAction) -> SideEffect<Self>
117+
118+
associatedtype Children = Never
117119
}
118120

119121
public extension ReducerProtocol where ReducerAction == Never {

Sources/SimplexArchitecture/Store/Store+send.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ extension Store {
7474
if _XCTIsTesting, let effectContext = EffectContext.id {
7575
let before = container.copy()
7676
sideEffect = withLock {
77-
reducer.reduce(into: container, action: action)
77+
reduce(container, action)
7878
}
7979
sentFromEffectActions.append(
8080
ActionTransition(
@@ -86,7 +86,7 @@ extension Store {
8686
)
8787
)
8888
} else {
89-
sideEffect = withLock { reducer.reduce(into: container, action: action) }
89+
sideEffect = withLock { reduce(container, action) }
9090
}
9191

9292
if case .none = sideEffect.kind {

Sources/SimplexArchitecture/Store/Store.swift

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,35 @@ public final class Store<Reducer: ReducerProtocol> {
2020
@usableFromInline var pullbackAction: ((Reducer.Action) -> Void)?
2121
@usableFromInline var pullbackReducerAction: ((Reducer.ReducerAction) -> Void)?
2222

23-
let reducer: Reducer
23+
let reduce: (StateContainer<Reducer.Target>, CombineAction<Reducer>) -> SideEffect<Reducer>
2424
var initialReducerState: (() -> Reducer.ReducerState)?
2525

2626
/// Initialize `Store` with the given reducer when the `ReducerState` is `Never`.
2727
public init(reducer: Reducer) where Reducer.ReducerState == Never {
28-
self.reducer = reducer
28+
self.reduce = reducer.reduce
2929
}
3030

3131
/// Initialize `Store` with the given `Reducer` and initial `ReducerState`.
3232
public init(
3333
reducer: Reducer,
3434
initialReducerState: @autoclosure @escaping () -> Reducer.ReducerState
3535
) {
36-
self.reducer = reducer
36+
self.reduce = reducer.reduce
37+
self.initialReducerState = initialReducerState
38+
}
39+
40+
public init<R: ReducerModifier<Reducer>>(
41+
reducer: R
42+
) where Reducer.ReducerState == Never {
43+
self.reduce = reducer.reduce
44+
}
45+
46+
/// Initialize `Store` with the given `Reducer` and initial `ReducerState`.
47+
public init<R: ReducerModifier<Reducer>>(
48+
reducer: R,
49+
initialReducerState: @autoclosure @escaping () -> Reducer.ReducerState
50+
) {
51+
self.reduce = reducer.reduce
3752
self.initialReducerState = initialReducerState
3853
}
3954

@@ -198,6 +213,11 @@ extension Store {
198213
}
199214
return [SendTask(task: task)]
200215

216+
case let .runEffects(effects):
217+
return effects.reduce(into: [SendTask]()) { partialResult, effect in
218+
partialResult += runEffect(effect, send: send)
219+
}
220+
201221
case .none:
202222
return []
203223
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
@testable import SimplexArchitecture
2+
import SwiftUI
3+
import Dependencies
4+
import XCTest
5+
6+
final class DependenciesOverrideModifierTests: XCTestCase {
7+
func testModifier() async {
8+
let base = BaseView(
9+
store: Store(
10+
reducer: _DependenciesOverrideModifier(base: BaseReducer()) {
11+
$0.test = .init(asyncThrows: {})
12+
}
13+
)
14+
)
15+
let container = base.store.setContainerIfNeeded(for: base, states: .init())
16+
await base.send(.test).wait()
17+
XCTAssertEqual(container.count, 1)
18+
}
19+
}
20+
21+
struct BaseReducer: ReducerProtocol {
22+
enum Action {
23+
case test
24+
case callback
25+
}
26+
27+
@Dependency(\.test) var test
28+
29+
func reduce(into state: StateContainer<BaseView>, action: Action) -> SideEffect<BaseReducer> {
30+
switch action {
31+
case .test:
32+
return .run { send in
33+
try await test.asyncThrows()
34+
await send(.callback)
35+
}
36+
case .callback:
37+
state.count += 1
38+
return .none
39+
}
40+
}
41+
}
42+
43+
@ScopeState
44+
struct BaseView: View {
45+
@State var count: Int = 0
46+
let store: Store<BaseReducer>
47+
48+
init(store: Store<BaseReducer> = .init(reducer: BaseReducer())) {
49+
self.store = store
50+
}
51+
52+
var body: some View { EmptyView() }
53+
}

Tests/SimplexArchitectureTests/ReducerTests.swift

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -98,17 +98,12 @@ final class ReducerTests: XCTestCase {
9898

9999
func testDependencies() async {
100100
let testStore = TestView(
101-
store: Store(
102-
reducer: withDependencies {
103-
$0.continuousClock = ImmediateClock()
104-
} operation: {
105-
TestReducer()
106-
},
101+
store: .init(
102+
reducer: TestReducer().dependency(\.test, value: .init {}),
107103
initialReducerState: .init()
108104
)
109105
).testStore(states: .init())
110-
111-
await testStore.send(.runEffectWithDependencies)
106+
await testStore.send(.testDependencies)
112107
await testStore.receive(.increment) {
113108
$0.count = 1
114109
}
@@ -134,6 +129,24 @@ final class ReducerTests: XCTestCase {
134129
}
135130
}
136131

132+
struct TestDependency: DependencyKey {
133+
public var asyncThrows: @Sendable () async throws -> Void
134+
135+
public init(asyncThrows: @Sendable @escaping () async throws -> Void) {
136+
self.asyncThrows = asyncThrows
137+
}
138+
139+
public static let liveValue: TestDependency = .init(asyncThrows: { throw CancellationError() })
140+
public static let testValue: TestDependency = .init(asyncThrows: {})
141+
}
142+
143+
extension DependencyValues {
144+
var test: TestDependency {
145+
get { self[TestDependency.self] }
146+
set { self[TestDependency.self] = newValue }
147+
}
148+
}
149+
137150
private struct TestReducer: ReducerProtocol {
138151
struct ReducerState: Equatable {
139152
var count = 0
@@ -157,9 +170,11 @@ private struct TestReducer: ReducerProtocol {
157170
case invokeDecrement
158171
case send
159172
case runEffectWithDependencies
173+
case testDependencies
160174
}
161175

162176
@Dependency(\.continuousClock) private var clock
177+
@Dependency(\.test) var test
163178

164179
func reduce(into state: StateContainer<TestView>, action: ReducerAction) -> SideEffect<Self> {
165180
switch action {
@@ -218,6 +233,12 @@ private struct TestReducer: ReducerProtocol {
218233
try await clock.sleep(for: .seconds(1))
219234
await send(.increment)
220235
}
236+
237+
case .testDependencies:
238+
return .run { send in
239+
try await test.asyncThrows()
240+
await send(.increment)
241+
}
221242
}
222243
}
223244
}
@@ -235,3 +256,27 @@ private struct TestView: View {
235256
EmptyView()
236257
}
237258
}
259+
260+
private struct MyReducer: ReducerProtocol {
261+
enum Action {
262+
case hoge
263+
}
264+
265+
func reduce(into state: StateContainer<MyView>, action: Action) -> SideEffect<MyReducer> {
266+
.none
267+
}
268+
}
269+
270+
@ScopeState
271+
private struct MyView: View {
272+
@State var count = 0
273+
let store: Store<MyReducer>
274+
275+
init(store: Store<MyReducer> = Store(reducer: MyReducer())) {
276+
self.store = store
277+
}
278+
279+
var body: some View {
280+
EmptyView()
281+
}
282+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import Foundation
2+
3+
typealias Original = @convention(thin) (UnownedJob) -> Void
4+
typealias Hook = @convention(thin) (UnownedJob, Original) -> Void
5+
6+
private let _swift_task_enqueueGlobal_hook = dlsym(
7+
dlopen(nil, 0), "swift_task_enqueueGlobal_hook"
8+
).assumingMemoryBound(to: Hook?.self)
9+
10+
var swift_task_enqueueGlobal_hook: Hook? {
11+
get { _swift_task_enqueueGlobal_hook.pointee }
12+
set { _swift_task_enqueueGlobal_hook.pointee = newValue }
13+
}

0 commit comments

Comments
 (0)