diff --git a/Sources/Example/GithubApp.swift b/Sources/Example/GithubApp.swift index 4671756..de365fa 100644 --- a/Sources/Example/GithubApp.swift +++ b/Sources/Example/GithubApp.swift @@ -85,7 +85,7 @@ actor SearchMiddleware: Middleware { typealias SearchStore = Store -struct SearchContainerView: View { +@MainActor struct SearchView: View { @State private var store = SearchStore( initialState: .init(), reducer: SearchReducer(), @@ -117,7 +117,7 @@ struct SearchContainerView: View { var body: some Scene { WindowGroup { NavigationStack { - SearchContainerView() + SearchView() } } } diff --git a/Sources/UnidirectionalFlow/Store.swift b/Sources/UnidirectionalFlow/Store.swift index 81dfec5..a9b260b 100644 --- a/Sources/UnidirectionalFlow/Store.swift +++ b/Sources/UnidirectionalFlow/Store.swift @@ -6,13 +6,11 @@ // import Observation -/// Type that stores the state of the app or feature. -@Observable @dynamicMemberLookup public final class Store { +/// Type that stores the state of the app or module allowing feeding actions. +@Observable @dynamicMemberLookup @MainActor public final class Store { private var state: State - private let reducer: any Reducer private let middlewares: any Collection> - private let lock = NSRecursiveLock() /// Creates an instance of `Store` with the folowing parameters. public init( @@ -27,28 +25,24 @@ import Observation /// A subscript providing access to the state of the store. public subscript(dynamicMember keyPath: KeyPath) -> T { - lock.withLock { state[keyPath: keyPath] } + state[keyPath: keyPath] } /// Use this method to mutate the state of the store by feeding actions. public func send(_ action: Action) async { - await apply(action) + apply(action) await intercept(action) } - @MainActor private func apply(_ action: Action) { - lock.withLock { - state = reducer.reduce(oldState: state, with: action) - } + private func apply(_ action: Action) { + state = reducer.reduce(oldState: state, with: action) } private func intercept(_ action: Action) async { - let capturedState = lock.withLock { state } - await withTaskGroup(of: Optional.self) { group in - middlewares.forEach { middleware in + for middleware in middlewares { group.addTask { - await middleware.process(state: capturedState, with: action) + await middleware.process(state: self.state, with: action) } } @@ -66,8 +60,8 @@ extension Store { deriveState: @escaping (State) -> DerivedState, deriveAction: @escaping (DerivedAction) -> Action ) -> Store { - let derived = Store( - initialState: lock.withLock { deriveState(state) }, + let store = Store( + initialState: deriveState(state), reducer: IdentityReducer(), middlewares: [ ClosureMiddleware { _, action in @@ -77,24 +71,34 @@ extension Store { ] ) - @Sendable func enableStateObservationTracking() { - withObservationTracking { - let newState = lock.withLock { deriveState(state) } - derived.lock.withLock { - if derived.state != newState { - derived.state = newState - } - } - } onChange: { - Task { - enableStateObservationTracking() - } + enableStateObservation( + for: store, + deriveState: deriveState, + deriveAction: deriveAction + ) + + return store + } + + private func enableStateObservation( + for store: Store, + deriveState: @escaping (State) -> DerivedState, + deriveAction: @escaping (DerivedAction) -> Action + ) { + withObservationTracking { + let newState = deriveState(state) + if store.state != newState { + store.state = newState + } + } onChange: { + Task { + await self.enableStateObservation( + for: store, + deriveState: deriveState, + deriveAction: deriveAction + ) } } - - enableStateObservationTracking() - - return derived } } @@ -107,13 +111,11 @@ extension Store { embed: @escaping (Value) -> Action ) -> Binding { .init( - get: { self.lock.withLock { extract(self.state) } }, + get: { extract(self.state) }, set: { newValue in let action = embed(newValue) - MainActor.assumeIsolated { - self.apply(action) - } + self.apply(action) Task { await self.intercept(action) diff --git a/Tests/UnidirectionalFlowTests/StoreTests.swift b/Tests/UnidirectionalFlowTests/StoreTests.swift index 0516558..541be80 100644 --- a/Tests/UnidirectionalFlowTests/StoreTests.swift +++ b/Tests/UnidirectionalFlowTests/StoreTests.swift @@ -47,7 +47,7 @@ final class StoreTests: XCTestCase { } } - func testSend() async { + @MainActor func testSend() async { let store = Store( initialState: .init(), reducer: TestReducer(), @@ -61,7 +61,7 @@ final class StoreTests: XCTestCase { XCTAssertEqual(store.counter, 0) } - func testMiddleware() async { + @MainActor func testMiddleware() async { let store = Store( initialState: .init(), reducer: TestReducer(), @@ -75,7 +75,7 @@ final class StoreTests: XCTestCase { XCTAssertEqual(store.counter, 1) } - func testMiddlewareCancellation() async { + @MainActor func testMiddlewareCancellation() async { let store = Store( initialState: .init(), reducer: TestReducer(), @@ -91,7 +91,7 @@ final class StoreTests: XCTestCase { XCTAssertEqual(store.counter, 0) } - func testDerivedStore() async throws { + @MainActor func testDerivedStore() async throws { let store = Store( initialState: .init(), reducer: TestReducer(), @@ -160,7 +160,7 @@ final class StoreTests: XCTestCase { XCTAssertEqual(store.counter, 10) } - func testThreadSafety() { + @MainActor func testThreadSafety() { measure { let store = Store( initialState: .init(),