From e5b82e0309454724f8e00e8efe640f62f49091cb Mon Sep 17 00:00:00 2001 From: RaghavBhasin Date: Wed, 21 Jun 2023 11:23:56 -0400 Subject: [PATCH 1/2] Allow syncing state with multiple reducers --- .../Reducers/SynchronizedStateReducer.swift | 223 +++++++++ .../SynchronizedStateReducerTests.swift | 439 ++++++++++++++++++ 2 files changed, 662 insertions(+) create mode 100644 Sources/ComposableArchitecture/Reducer/Reducers/SynchronizedStateReducer.swift create mode 100644 Tests/ComposableArchitectureTests/SynchronizedStateReducerTests.swift diff --git a/Sources/ComposableArchitecture/Reducer/Reducers/SynchronizedStateReducer.swift b/Sources/ComposableArchitecture/Reducer/Reducers/SynchronizedStateReducer.swift new file mode 100644 index 000000000000..10eb5694beac --- /dev/null +++ b/Sources/ComposableArchitecture/Reducer/Reducers/SynchronizedStateReducer.swift @@ -0,0 +1,223 @@ +import Foundation + +/// Parameters for `withSynchronizedState` for `Reducer`. +/// This allows providing the reducer with what state to watch and how to +/// synchronize it with parent and other siblings. +public struct SynchronizationParameters { + + public init( + parent: SynchronizationType, + children: [SynchronizationType] + ) { + self.parent = parent + self.children = children + } + + var parent: SynchronizationType + var children: [SynchronizationType] + + /// Defines how to synchronize state amongst the reducers. + public enum SynchronizationType { + + /// Only observe this piece of state for changes. + case observeOnly(KeyPath) + + /// Only update this piece of state if anything changes. But do not + /// propagate its changes to parent or siblings. + case updateOnly(WritableKeyPath) + + /// Update this piece of state if anything else on the parent changed + /// or either of the siblings change. Also if this state changes itself + /// communicate that to parent and siblings. + case synchronize(WritableKeyPath) + } +} + +extension ReducerProtocol { + /// Allows observing a piece of state and synchronizing it across + /// a `Reducer`'s children. This can be configured to either only + /// observe or update the state, or all of that if needed. + /// + /// The order of priority for state changes assumes that parent state if observable will + /// supersede the children state. However, since the reducer does execute one action at a time + /// if used correctly this should not be an issue. The order of priority amongst the children + /// is just the first change in the `children` array. + /// + /// For example, if a parent feature holds onto a piece state that is needed by its children + /// _and_ the state can be mutated `synchronizeState` operator can allow ensuring + /// the states stay in-sync without additional actions to keep it up to date. + /// + /// ```swift + /// struct Child: ReducerProtocol { + /// struct State { + /// var sharedState: Foo + /// // .. + /// } + /// + /// enum Action ... + /// } + /// struct Parent: ReducerProtocol { + /// struct State { + /// var sharedState: Foo + /// var child: Child.State + /// // ... + /// } + /// enum Action { + /// case child(Child.Action) + /// // ... + /// } + /// + /// var body: some ReducerProtocol { + /// Reduce { state, action in + /// Scope(state: \.child, action: /Action.child) { + /// Child() + /// } + /// + /// // Core logic for parent feature + /// } + /// .synchronizeState( + /// over: SynchronizationParameters( + /// parent: .observeOnly(\State.sharedState), + /// children: [ + /// .synchronize(\State.child.sharedState) + /// ] + /// ) + /// ) + /// } + /// } + /// ``` + public func synchronizeState( + over synchronizationParameters: SynchronizationParameters, + fileID: StaticString = #fileID, + line: UInt = #line + ) -> _SynchronizedStateReducer { + return _SynchronizedStateReducer( + parent: self, + synchronizationParameters: synchronizationParameters, + fileID: fileID, + line: line + ) + } +} + +public struct _SynchronizedStateReducer: ReducerProtocol +{ + @usableFromInline + let parent: Parent + + @usableFromInline + let fileID: StaticString + + @usableFromInline + let line: UInt + + @usableFromInline + let synchronizationParameters: SynchronizationParameters + + @usableFromInline + init( + parent: Parent, + synchronizationParameters: SynchronizationParameters, + fileID: StaticString, + line: UInt + ) { + self.parent = parent + self.synchronizationParameters = synchronizationParameters + self.fileID = fileID + self.line = line + } + + public func reduce( + into state: inout Parent.State, action: Parent.Action + ) -> EffectTask { + + // Get parent and children states before running the reducer. + let parentStateBeforeTransformation = state[keyPath: synchronizationParameters.parent.keypath] + let childrenStateBeforeTransformation = synchronizationParameters.children.map { + childParam -> Value? in + if let keypath = childParam.observableKeypath { + return state[keyPath: keypath] + } + return nil + } + + let effects = self.parent.reduce(into: &state, action: action) + + // If we can observe the parent and parent state changed, then + // write the new state and return effects. + if let observable = synchronizationParameters.parent.observableKeypath, + state[keyPath: observable] != parentStateBeforeTransformation + { + synchronizationParameters.children + .compactMap { $0.writableKeypath } + .forEach { keypath in + state[keyPath: keypath] = state[keyPath: observable] + } + + return effects + } + + // If we can observe the parent, then check for state changes + // with children and pick the first change. + let childrenStateAfterTransformation = synchronizationParameters.children.map { + childParam -> Value? in + if let keypath = childParam.observableKeypath { + return state[keyPath: keypath] + } + return nil + } + + if let newState = zip(childrenStateBeforeTransformation, childrenStateAfterTransformation) + .first(where: { $0 != $1 })?.1 + { + + // We can update the parent and other siblings that are allowed. + ([synchronizationParameters.parent.writableKeypath] + + synchronizationParameters.children.map(\.writableKeypath)) + .compactMap { $0 } + .forEach { + state[keyPath: $0] = newState + } + } + + return effects + } +} + +extension SynchronizationParameters.SynchronizationType { + /// Get a read only keypath to observe for changes. + var observableKeypath: KeyPath? { + switch self { + case .observeOnly(let keyPath): + return keyPath + case .updateOnly: + return nil + case .synchronize(let writableKeyPath): + return writableKeyPath + } + } + + /// Get a writable keypath to update for changes. + var writableKeypath: WritableKeyPath? { + switch self { + case .observeOnly: + return nil + case .updateOnly(let writableKeyPath): + return writableKeyPath + case .synchronize(let writableKeyPath): + return writableKeyPath + } + } + + /// Get a read only keypath to observe for changes. + var keypath: KeyPath { + switch self { + case .observeOnly(let keyPath): + return keyPath + case .updateOnly(let keyPath): + return keyPath + case .synchronize(let keyPath): + return keyPath + } + } +} diff --git a/Tests/ComposableArchitectureTests/SynchronizedStateReducerTests.swift b/Tests/ComposableArchitectureTests/SynchronizedStateReducerTests.swift new file mode 100644 index 000000000000..9c94701511c0 --- /dev/null +++ b/Tests/ComposableArchitectureTests/SynchronizedStateReducerTests.swift @@ -0,0 +1,439 @@ +import ComposableArchitecture +@_spi(Concurrency) import Dependencies +import XCTest + +@MainActor +final class SynchronizedStateReducerTests: BaseTCATestCase { + + func testChildrenStateUpdateChangesParent() { + struct SingleChildReducer: ReducerProtocol { + typealias State = Parent + typealias Action = Void + + var body: some ReducerProtocol { + Reduce { state, _ in + state.child.foo += 1 + return .none + } + .synchronizeState( + over: .init( + parent: .synchronize(\Parent.foo), + children: [ + .synchronize(\Parent.child.foo) + ] + ) + ) + } + } + + let reducer = SingleChildReducer() + var state = Parent( + foo: 0, + child: .init(foo: 0) + ) + _ = reducer.reduce(into: &state, action: ()) + XCTAssertEqual(1, state.foo) + XCTAssertEqual(1, state.child.foo) + } + + func testParentStateUpdateChangesChildrenState() { + struct MultiChildReducer: ReducerProtocol { + typealias State = ParentMultiChild + typealias Action = Void + + var body: some ReducerProtocol { + Reduce { state, _ in + state.foo += 1 + return .none + } + .synchronizeState( + over: .init( + parent: .synchronize(\ParentMultiChild.foo), + children: [ + .synchronize(\ParentMultiChild.child1.foo), + .synchronize(\ParentMultiChild.child2.foo), + .synchronize(\ParentMultiChild.child3.foo) + ] + ) + ) + } + } + + let reducer = MultiChildReducer() + var state = ParentMultiChild( + foo: 0, + child1: .init(foo: 0), + child2: .init(foo: 0), + child3: .init(foo: 0) + ) + + _ = reducer.reduce(into: &state, action: ()) + XCTAssertEqual(1, state.foo) + XCTAssertEqual(1, state.child1.foo) + XCTAssertEqual(1, state.child2.foo) + XCTAssertEqual(1, state.child3.foo) + } + + func testWriteOnlyParentStateChangeDoesNotUpdateChild() { + struct SingleChildReducer: ReducerProtocol { + typealias State = Parent + typealias Action = Void + + var body: some ReducerProtocol { + Reduce { state, _ in + state.foo += 1 + return .none + } + .synchronizeState( + over: .init( + parent: .updateOnly(\Parent.foo), + children: [ + .synchronize(\Parent.child.foo), + ] + ) + ) + } + } + + let reducer = SingleChildReducer() + var state = Parent( + foo: 0, + child: .init(foo: 0) + ) + _ = reducer.reduce(into: &state, action: ()) + XCTAssertEqual(1, state.foo) + XCTAssertEqual(0, state.child.foo) + } + + func testParentStateChangeDoesNotUpdateReadOnlyChild() { + struct SingleChildReducer: ReducerProtocol { + typealias State = Parent + typealias Action = Void + + var body: some ReducerProtocol { + Reduce { state, _ in + state.foo += 1 + return .none + } + .synchronizeState( + over: .init( + parent: .synchronize(\Parent.foo), + children: [ + .observeOnly(\Parent.child.foo), + ] + ) + ) + } + } + + let reducer = SingleChildReducer() + var state = Parent( + foo: 0, + child: .init(foo: 0) + ) + _ = reducer.reduce(into: &state, action: ()) + XCTAssertEqual(1, state.foo) + XCTAssertEqual(0, state.child.foo) + } + + func testReadOnlyParentWithReadWriteChild() { + struct SingleChildReducer: ReducerProtocol { + typealias State = Parent + typealias Action = ParentAction + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .updateChild: + state.child.foo += 1 + case .updateParent: + state.foo += 2 + } + return .none + } + .synchronizeState( + over: .init( + parent: .updateOnly(\Parent.foo), + children: [ + .synchronize(\Parent.child.foo), + ] + ) + ) + } + } + + let reducer = SingleChildReducer() + var state = Parent( + foo: 0, + child: .init(foo: 0) + ) + // Update parent and nothing changes on child. + _ = reducer.reduce(into: &state, action: .updateParent) + XCTAssertEqual(2, state.foo) + XCTAssertEqual(0, state.child.foo) + + + // Update child and it changes on parent too. + _ = reducer.reduce(into: &state, action: .updateChild) + XCTAssertEqual(1, state.foo) + XCTAssertEqual(1, state.child.foo) + } + + func testParentWithReadOnlyChild() { + struct SingleChildReducer: ReducerProtocol { + typealias State = Parent + typealias Action = ParentAction + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .updateChild: + state.child.foo += 1 + case .updateParent: + state.foo += 2 + } + return .none + } + .synchronizeState( + over: .init( + parent: .synchronize(\Parent.foo), + children: [ + .observeOnly(\Parent.child.foo), + ] + ) + ) + } + } + + let reducer = SingleChildReducer() + var state = Parent( + foo: 0, + child: .init(foo: 0) + ) + + // Update parent and nothing changes on child. + _ = reducer.reduce(into: &state, action: .updateParent) + XCTAssertEqual(2, state.foo) + XCTAssertEqual(0, state.child.foo) + + + // Update child and parent changes too. + _ = reducer.reduce(into: &state, action: .updateChild) + XCTAssertEqual(1, state.foo) + XCTAssertEqual(1, state.child.foo) + } + + func testMultiChildComboWithReadWriteParent() { + struct MultiChildReducer: ReducerProtocol { + typealias State = ParentMultiChild + typealias Action = ParentMultiChildAction + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .updateChild1: + state.child1.foo += 1 + case .updateChild2: + state.child2.foo += 2 + case .updateChild3: + state.child3.foo += 3 + case .updateParent: + state.foo += 20 + } + return .none + } + .synchronizeState( + over: .init( + parent: .synchronize(\ParentMultiChild.foo), + children: [ + .observeOnly(\ParentMultiChild.child1.foo), + .synchronize(\ParentMultiChild.child2.foo), + .updateOnly(\ParentMultiChild.child3.foo), + ] + ) + ) + } + } + + let reducer = MultiChildReducer() + var state = ParentMultiChild( + foo: 0, + child1: .init(foo: 0), + child2: .init(foo: 0), + child3: .init(foo: 0) + ) + + // Update parent and child 2 and child 3 get updated.. + _ = reducer.reduce(into: &state, action: .updateParent) + XCTAssertEqual(20, state.foo) + XCTAssertEqual(0, state.child1.foo) + XCTAssertEqual(20, state.child2.foo) + XCTAssertEqual(20, state.child3.foo) + + // Update child 3 and nothing changes with others, since it is not tracked for changes. + _ = reducer.reduce(into: &state, action: .updateChild3) + XCTAssertEqual(20, state.foo) + XCTAssertEqual(0, state.child1.foo) + XCTAssertEqual(20, state.child2.foo) + XCTAssertEqual(23, state.child3.foo) + + // Update child 1 and other writable ones change. + _ = reducer.reduce(into: &state, action: .updateChild1) + XCTAssertEqual(1, state.foo) + XCTAssertEqual(1, state.child1.foo) + XCTAssertEqual(1, state.child2.foo) + XCTAssertEqual(1, state.child3.foo) + } + + func testMultiChildComboWithReadOnlyParent() { + struct MultiChildReducer: ReducerProtocol { + typealias State = ParentMultiChild + typealias Action = ParentMultiChildAction + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .updateChild1: + state.child1.foo += 1 + case .updateChild2: + state.child2.foo += 2 + case .updateChild3: + state.child3.foo += 3 + case .updateParent: + state.foo += 20 + } + return .none + } + .synchronizeState( + over: .init( + parent: .observeOnly(\ParentMultiChild.foo), + children: [ + .observeOnly(\ParentMultiChild.child1.foo), + .synchronize(\ParentMultiChild.child2.foo), + .updateOnly(\ParentMultiChild.child3.foo), + ] + ) + ) + } + } + + let reducer = MultiChildReducer() + var state = ParentMultiChild( + foo: 0, + child1: .init(foo: 0), + child2: .init(foo: 0), + child3: .init(foo: 0) + ) + + // Update parent and child 2 and child 3 get updated.. + _ = reducer.reduce(into: &state, action: .updateParent) + XCTAssertEqual(20, state.foo) + XCTAssertEqual(0, state.child1.foo) + XCTAssertEqual(20, state.child2.foo) + XCTAssertEqual(20, state.child3.foo) + + // Update child 3 and nothing changes with others, since it is not tracked for changes. + _ = reducer.reduce(into: &state, action: .updateChild3) + XCTAssertEqual(20, state.foo) + XCTAssertEqual(0, state.child1.foo) + XCTAssertEqual(20, state.child2.foo) + XCTAssertEqual(23, state.child3.foo) + + // Update child 1 and other writable ones change. + _ = reducer.reduce(into: &state, action: .updateChild1) + XCTAssertEqual(20, state.foo) // Parent is read only. + XCTAssertEqual(1, state.child1.foo) + XCTAssertEqual(1, state.child2.foo) + XCTAssertEqual(1, state.child3.foo) + } + + func testMultiChildComboWithWriteOnlyParent() { + struct MultiChildReducer: ReducerProtocol { + typealias State = ParentMultiChild + typealias Action = ParentMultiChildAction + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .updateChild1: + state.child1.foo += 1 + case .updateChild2: + state.child2.foo += 2 + case .updateChild3: + state.child3.foo += 3 + case .updateParent: + state.foo += 20 + } + return .none + } + .synchronizeState( + over: .init( + parent: .updateOnly(\ParentMultiChild.foo), + children: [ + .observeOnly(\ParentMultiChild.child1.foo), + .synchronize(\ParentMultiChild.child2.foo), + .updateOnly(\ParentMultiChild.child3.foo), + ] + ) + ) + } + } + + let reducer = MultiChildReducer() + var state = ParentMultiChild( + foo: 0, + child1: .init(foo: 0), + child2: .init(foo: 0), + child3: .init(foo: 0) + ) + + // Update parent and nothing else changes becuase parent is write only. + _ = reducer.reduce(into: &state, action: .updateParent) + XCTAssertEqual(20, state.foo) + XCTAssertEqual(0, state.child1.foo) + XCTAssertEqual(0, state.child2.foo) + XCTAssertEqual(0, state.child3.foo) + + // Update child 3 and nothing changes with others, since it is not tracked for changes. + _ = reducer.reduce(into: &state, action: .updateChild3) + XCTAssertEqual(20, state.foo) + XCTAssertEqual(0, state.child1.foo) + XCTAssertEqual(0, state.child2.foo) + XCTAssertEqual(3, state.child3.foo) + + // Update child 1 and other writable ones change. + _ = reducer.reduce(into: &state, action: .updateChild1) + XCTAssertEqual(1, state.foo) // Parent is write only. + XCTAssertEqual(1, state.child1.foo) + XCTAssertEqual(1, state.child2.foo) + XCTAssertEqual(1, state.child3.foo) + } +} + +struct Parent: Equatable { + var foo: Int + var child: Child +} + +enum ParentAction: Equatable { + case updateParent + case updateChild +} + +enum ParentMultiChildAction: Equatable { + case updateParent + case updateChild1 + case updateChild2 + case updateChild3 +} + +struct ParentMultiChild: Equatable { + var foo: Int + var child1: Child + var child2: Child + var child3: Child +} + +struct Child: Equatable { + var foo: Int +} From 7aabcf9b0e8728fb48740674abcb5e2301c5200a Mon Sep 17 00:00:00 2001 From: raghavbhasin97 Date: Wed, 21 Jun 2023 17:45:14 +0000 Subject: [PATCH 2/2] Run swift-format --- .../SynchronizedStateReducerTests.swift | 760 +++++++++--------- 1 file changed, 379 insertions(+), 381 deletions(-) diff --git a/Tests/ComposableArchitectureTests/SynchronizedStateReducerTests.swift b/Tests/ComposableArchitectureTests/SynchronizedStateReducerTests.swift index 9c94701511c0..919fc2da792c 100644 --- a/Tests/ComposableArchitectureTests/SynchronizedStateReducerTests.swift +++ b/Tests/ComposableArchitectureTests/SynchronizedStateReducerTests.swift @@ -5,435 +5,433 @@ import XCTest @MainActor final class SynchronizedStateReducerTests: BaseTCATestCase { - func testChildrenStateUpdateChangesParent() { - struct SingleChildReducer: ReducerProtocol { - typealias State = Parent - typealias Action = Void - - var body: some ReducerProtocol { - Reduce { state, _ in - state.child.foo += 1 - return .none - } - .synchronizeState( - over: .init( - parent: .synchronize(\Parent.foo), - children: [ - .synchronize(\Parent.child.foo) - ] - ) - ) - } + func testChildrenStateUpdateChangesParent() { + struct SingleChildReducer: ReducerProtocol { + typealias State = Parent + typealias Action = Void + + var body: some ReducerProtocol { + Reduce { state, _ in + state.child.foo += 1 + return .none } - - let reducer = SingleChildReducer() - var state = Parent( - foo: 0, - child: .init(foo: 0) + .synchronizeState( + over: .init( + parent: .synchronize(\Parent.foo), + children: [ + .synchronize(\Parent.child.foo) + ] + ) ) - _ = reducer.reduce(into: &state, action: ()) - XCTAssertEqual(1, state.foo) - XCTAssertEqual(1, state.child.foo) + } } - func testParentStateUpdateChangesChildrenState() { - struct MultiChildReducer: ReducerProtocol { - typealias State = ParentMultiChild - typealias Action = Void - - var body: some ReducerProtocol { - Reduce { state, _ in - state.foo += 1 - return .none - } - .synchronizeState( - over: .init( - parent: .synchronize(\ParentMultiChild.foo), - children: [ - .synchronize(\ParentMultiChild.child1.foo), - .synchronize(\ParentMultiChild.child2.foo), - .synchronize(\ParentMultiChild.child3.foo) - ] - ) - ) - } + let reducer = SingleChildReducer() + var state = Parent( + foo: 0, + child: .init(foo: 0) + ) + _ = reducer.reduce(into: &state, action: ()) + XCTAssertEqual(1, state.foo) + XCTAssertEqual(1, state.child.foo) + } + + func testParentStateUpdateChangesChildrenState() { + struct MultiChildReducer: ReducerProtocol { + typealias State = ParentMultiChild + typealias Action = Void + + var body: some ReducerProtocol { + Reduce { state, _ in + state.foo += 1 + return .none } - - let reducer = MultiChildReducer() - var state = ParentMultiChild( - foo: 0, - child1: .init(foo: 0), - child2: .init(foo: 0), - child3: .init(foo: 0) + .synchronizeState( + over: .init( + parent: .synchronize(\ParentMultiChild.foo), + children: [ + .synchronize(\ParentMultiChild.child1.foo), + .synchronize(\ParentMultiChild.child2.foo), + .synchronize(\ParentMultiChild.child3.foo), + ] + ) ) - - _ = reducer.reduce(into: &state, action: ()) - XCTAssertEqual(1, state.foo) - XCTAssertEqual(1, state.child1.foo) - XCTAssertEqual(1, state.child2.foo) - XCTAssertEqual(1, state.child3.foo) + } } - func testWriteOnlyParentStateChangeDoesNotUpdateChild() { - struct SingleChildReducer: ReducerProtocol { - typealias State = Parent - typealias Action = Void - - var body: some ReducerProtocol { - Reduce { state, _ in - state.foo += 1 - return .none - } - .synchronizeState( - over: .init( - parent: .updateOnly(\Parent.foo), - children: [ - .synchronize(\Parent.child.foo), - ] - ) - ) - } + let reducer = MultiChildReducer() + var state = ParentMultiChild( + foo: 0, + child1: .init(foo: 0), + child2: .init(foo: 0), + child3: .init(foo: 0) + ) + + _ = reducer.reduce(into: &state, action: ()) + XCTAssertEqual(1, state.foo) + XCTAssertEqual(1, state.child1.foo) + XCTAssertEqual(1, state.child2.foo) + XCTAssertEqual(1, state.child3.foo) + } + + func testWriteOnlyParentStateChangeDoesNotUpdateChild() { + struct SingleChildReducer: ReducerProtocol { + typealias State = Parent + typealias Action = Void + + var body: some ReducerProtocol { + Reduce { state, _ in + state.foo += 1 + return .none } - - let reducer = SingleChildReducer() - var state = Parent( - foo: 0, - child: .init(foo: 0) + .synchronizeState( + over: .init( + parent: .updateOnly(\Parent.foo), + children: [ + .synchronize(\Parent.child.foo) + ] + ) ) - _ = reducer.reduce(into: &state, action: ()) - XCTAssertEqual(1, state.foo) - XCTAssertEqual(0, state.child.foo) + } } - func testParentStateChangeDoesNotUpdateReadOnlyChild() { - struct SingleChildReducer: ReducerProtocol { - typealias State = Parent - typealias Action = Void - - var body: some ReducerProtocol { - Reduce { state, _ in - state.foo += 1 - return .none - } - .synchronizeState( - over: .init( - parent: .synchronize(\Parent.foo), - children: [ - .observeOnly(\Parent.child.foo), - ] - ) - ) - } + let reducer = SingleChildReducer() + var state = Parent( + foo: 0, + child: .init(foo: 0) + ) + _ = reducer.reduce(into: &state, action: ()) + XCTAssertEqual(1, state.foo) + XCTAssertEqual(0, state.child.foo) + } + + func testParentStateChangeDoesNotUpdateReadOnlyChild() { + struct SingleChildReducer: ReducerProtocol { + typealias State = Parent + typealias Action = Void + + var body: some ReducerProtocol { + Reduce { state, _ in + state.foo += 1 + return .none } - - let reducer = SingleChildReducer() - var state = Parent( - foo: 0, - child: .init(foo: 0) + .synchronizeState( + over: .init( + parent: .synchronize(\Parent.foo), + children: [ + .observeOnly(\Parent.child.foo) + ] + ) ) - _ = reducer.reduce(into: &state, action: ()) - XCTAssertEqual(1, state.foo) - XCTAssertEqual(0, state.child.foo) + } } - func testReadOnlyParentWithReadWriteChild() { - struct SingleChildReducer: ReducerProtocol { - typealias State = Parent - typealias Action = ParentAction - - var body: some ReducerProtocol { - Reduce { state, action in - switch action { - case .updateChild: - state.child.foo += 1 - case .updateParent: - state.foo += 2 - } - return .none - } - .synchronizeState( - over: .init( - parent: .updateOnly(\Parent.foo), - children: [ - .synchronize(\Parent.child.foo), - ] - ) - ) - } + let reducer = SingleChildReducer() + var state = Parent( + foo: 0, + child: .init(foo: 0) + ) + _ = reducer.reduce(into: &state, action: ()) + XCTAssertEqual(1, state.foo) + XCTAssertEqual(0, state.child.foo) + } + + func testReadOnlyParentWithReadWriteChild() { + struct SingleChildReducer: ReducerProtocol { + typealias State = Parent + typealias Action = ParentAction + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .updateChild: + state.child.foo += 1 + case .updateParent: + state.foo += 2 + } + return .none } - - let reducer = SingleChildReducer() - var state = Parent( - foo: 0, - child: .init(foo: 0) + .synchronizeState( + over: .init( + parent: .updateOnly(\Parent.foo), + children: [ + .synchronize(\Parent.child.foo) + ] + ) ) - // Update parent and nothing changes on child. - _ = reducer.reduce(into: &state, action: .updateParent) - XCTAssertEqual(2, state.foo) - XCTAssertEqual(0, state.child.foo) - - - // Update child and it changes on parent too. - _ = reducer.reduce(into: &state, action: .updateChild) - XCTAssertEqual(1, state.foo) - XCTAssertEqual(1, state.child.foo) + } } - func testParentWithReadOnlyChild() { - struct SingleChildReducer: ReducerProtocol { - typealias State = Parent - typealias Action = ParentAction - - var body: some ReducerProtocol { - Reduce { state, action in - switch action { - case .updateChild: - state.child.foo += 1 - case .updateParent: - state.foo += 2 - } - return .none - } - .synchronizeState( - over: .init( - parent: .synchronize(\Parent.foo), - children: [ - .observeOnly(\Parent.child.foo), - ] - ) - ) - } + let reducer = SingleChildReducer() + var state = Parent( + foo: 0, + child: .init(foo: 0) + ) + // Update parent and nothing changes on child. + _ = reducer.reduce(into: &state, action: .updateParent) + XCTAssertEqual(2, state.foo) + XCTAssertEqual(0, state.child.foo) + + // Update child and it changes on parent too. + _ = reducer.reduce(into: &state, action: .updateChild) + XCTAssertEqual(1, state.foo) + XCTAssertEqual(1, state.child.foo) + } + + func testParentWithReadOnlyChild() { + struct SingleChildReducer: ReducerProtocol { + typealias State = Parent + typealias Action = ParentAction + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .updateChild: + state.child.foo += 1 + case .updateParent: + state.foo += 2 + } + return .none } - - let reducer = SingleChildReducer() - var state = Parent( - foo: 0, - child: .init(foo: 0) + .synchronizeState( + over: .init( + parent: .synchronize(\Parent.foo), + children: [ + .observeOnly(\Parent.child.foo) + ] + ) ) - - // Update parent and nothing changes on child. - _ = reducer.reduce(into: &state, action: .updateParent) - XCTAssertEqual(2, state.foo) - XCTAssertEqual(0, state.child.foo) - - - // Update child and parent changes too. - _ = reducer.reduce(into: &state, action: .updateChild) - XCTAssertEqual(1, state.foo) - XCTAssertEqual(1, state.child.foo) + } } - func testMultiChildComboWithReadWriteParent() { - struct MultiChildReducer: ReducerProtocol { - typealias State = ParentMultiChild - typealias Action = ParentMultiChildAction - - var body: some ReducerProtocol { - Reduce { state, action in - switch action { - case .updateChild1: - state.child1.foo += 1 - case .updateChild2: - state.child2.foo += 2 - case .updateChild3: - state.child3.foo += 3 - case .updateParent: - state.foo += 20 - } - return .none - } - .synchronizeState( - over: .init( - parent: .synchronize(\ParentMultiChild.foo), - children: [ - .observeOnly(\ParentMultiChild.child1.foo), - .synchronize(\ParentMultiChild.child2.foo), - .updateOnly(\ParentMultiChild.child3.foo), - ] - ) - ) - } + let reducer = SingleChildReducer() + var state = Parent( + foo: 0, + child: .init(foo: 0) + ) + + // Update parent and nothing changes on child. + _ = reducer.reduce(into: &state, action: .updateParent) + XCTAssertEqual(2, state.foo) + XCTAssertEqual(0, state.child.foo) + + // Update child and parent changes too. + _ = reducer.reduce(into: &state, action: .updateChild) + XCTAssertEqual(1, state.foo) + XCTAssertEqual(1, state.child.foo) + } + + func testMultiChildComboWithReadWriteParent() { + struct MultiChildReducer: ReducerProtocol { + typealias State = ParentMultiChild + typealias Action = ParentMultiChildAction + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .updateChild1: + state.child1.foo += 1 + case .updateChild2: + state.child2.foo += 2 + case .updateChild3: + state.child3.foo += 3 + case .updateParent: + state.foo += 20 + } + return .none } - - let reducer = MultiChildReducer() - var state = ParentMultiChild( - foo: 0, - child1: .init(foo: 0), - child2: .init(foo: 0), - child3: .init(foo: 0) + .synchronizeState( + over: .init( + parent: .synchronize(\ParentMultiChild.foo), + children: [ + .observeOnly(\ParentMultiChild.child1.foo), + .synchronize(\ParentMultiChild.child2.foo), + .updateOnly(\ParentMultiChild.child3.foo), + ] + ) ) - - // Update parent and child 2 and child 3 get updated.. - _ = reducer.reduce(into: &state, action: .updateParent) - XCTAssertEqual(20, state.foo) - XCTAssertEqual(0, state.child1.foo) - XCTAssertEqual(20, state.child2.foo) - XCTAssertEqual(20, state.child3.foo) - - // Update child 3 and nothing changes with others, since it is not tracked for changes. - _ = reducer.reduce(into: &state, action: .updateChild3) - XCTAssertEqual(20, state.foo) - XCTAssertEqual(0, state.child1.foo) - XCTAssertEqual(20, state.child2.foo) - XCTAssertEqual(23, state.child3.foo) - - // Update child 1 and other writable ones change. - _ = reducer.reduce(into: &state, action: .updateChild1) - XCTAssertEqual(1, state.foo) - XCTAssertEqual(1, state.child1.foo) - XCTAssertEqual(1, state.child2.foo) - XCTAssertEqual(1, state.child3.foo) + } } - func testMultiChildComboWithReadOnlyParent() { - struct MultiChildReducer: ReducerProtocol { - typealias State = ParentMultiChild - typealias Action = ParentMultiChildAction - - var body: some ReducerProtocol { - Reduce { state, action in - switch action { - case .updateChild1: - state.child1.foo += 1 - case .updateChild2: - state.child2.foo += 2 - case .updateChild3: - state.child3.foo += 3 - case .updateParent: - state.foo += 20 - } - return .none - } - .synchronizeState( - over: .init( - parent: .observeOnly(\ParentMultiChild.foo), - children: [ - .observeOnly(\ParentMultiChild.child1.foo), - .synchronize(\ParentMultiChild.child2.foo), - .updateOnly(\ParentMultiChild.child3.foo), - ] - ) - ) - } + let reducer = MultiChildReducer() + var state = ParentMultiChild( + foo: 0, + child1: .init(foo: 0), + child2: .init(foo: 0), + child3: .init(foo: 0) + ) + + // Update parent and child 2 and child 3 get updated.. + _ = reducer.reduce(into: &state, action: .updateParent) + XCTAssertEqual(20, state.foo) + XCTAssertEqual(0, state.child1.foo) + XCTAssertEqual(20, state.child2.foo) + XCTAssertEqual(20, state.child3.foo) + + // Update child 3 and nothing changes with others, since it is not tracked for changes. + _ = reducer.reduce(into: &state, action: .updateChild3) + XCTAssertEqual(20, state.foo) + XCTAssertEqual(0, state.child1.foo) + XCTAssertEqual(20, state.child2.foo) + XCTAssertEqual(23, state.child3.foo) + + // Update child 1 and other writable ones change. + _ = reducer.reduce(into: &state, action: .updateChild1) + XCTAssertEqual(1, state.foo) + XCTAssertEqual(1, state.child1.foo) + XCTAssertEqual(1, state.child2.foo) + XCTAssertEqual(1, state.child3.foo) + } + + func testMultiChildComboWithReadOnlyParent() { + struct MultiChildReducer: ReducerProtocol { + typealias State = ParentMultiChild + typealias Action = ParentMultiChildAction + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .updateChild1: + state.child1.foo += 1 + case .updateChild2: + state.child2.foo += 2 + case .updateChild3: + state.child3.foo += 3 + case .updateParent: + state.foo += 20 + } + return .none } - - let reducer = MultiChildReducer() - var state = ParentMultiChild( - foo: 0, - child1: .init(foo: 0), - child2: .init(foo: 0), - child3: .init(foo: 0) + .synchronizeState( + over: .init( + parent: .observeOnly(\ParentMultiChild.foo), + children: [ + .observeOnly(\ParentMultiChild.child1.foo), + .synchronize(\ParentMultiChild.child2.foo), + .updateOnly(\ParentMultiChild.child3.foo), + ] + ) ) - - // Update parent and child 2 and child 3 get updated.. - _ = reducer.reduce(into: &state, action: .updateParent) - XCTAssertEqual(20, state.foo) - XCTAssertEqual(0, state.child1.foo) - XCTAssertEqual(20, state.child2.foo) - XCTAssertEqual(20, state.child3.foo) - - // Update child 3 and nothing changes with others, since it is not tracked for changes. - _ = reducer.reduce(into: &state, action: .updateChild3) - XCTAssertEqual(20, state.foo) - XCTAssertEqual(0, state.child1.foo) - XCTAssertEqual(20, state.child2.foo) - XCTAssertEqual(23, state.child3.foo) - - // Update child 1 and other writable ones change. - _ = reducer.reduce(into: &state, action: .updateChild1) - XCTAssertEqual(20, state.foo) // Parent is read only. - XCTAssertEqual(1, state.child1.foo) - XCTAssertEqual(1, state.child2.foo) - XCTAssertEqual(1, state.child3.foo) + } } - func testMultiChildComboWithWriteOnlyParent() { - struct MultiChildReducer: ReducerProtocol { - typealias State = ParentMultiChild - typealias Action = ParentMultiChildAction - - var body: some ReducerProtocol { - Reduce { state, action in - switch action { - case .updateChild1: - state.child1.foo += 1 - case .updateChild2: - state.child2.foo += 2 - case .updateChild3: - state.child3.foo += 3 - case .updateParent: - state.foo += 20 - } - return .none - } - .synchronizeState( - over: .init( - parent: .updateOnly(\ParentMultiChild.foo), - children: [ - .observeOnly(\ParentMultiChild.child1.foo), - .synchronize(\ParentMultiChild.child2.foo), - .updateOnly(\ParentMultiChild.child3.foo), - ] - ) - ) - } + let reducer = MultiChildReducer() + var state = ParentMultiChild( + foo: 0, + child1: .init(foo: 0), + child2: .init(foo: 0), + child3: .init(foo: 0) + ) + + // Update parent and child 2 and child 3 get updated.. + _ = reducer.reduce(into: &state, action: .updateParent) + XCTAssertEqual(20, state.foo) + XCTAssertEqual(0, state.child1.foo) + XCTAssertEqual(20, state.child2.foo) + XCTAssertEqual(20, state.child3.foo) + + // Update child 3 and nothing changes with others, since it is not tracked for changes. + _ = reducer.reduce(into: &state, action: .updateChild3) + XCTAssertEqual(20, state.foo) + XCTAssertEqual(0, state.child1.foo) + XCTAssertEqual(20, state.child2.foo) + XCTAssertEqual(23, state.child3.foo) + + // Update child 1 and other writable ones change. + _ = reducer.reduce(into: &state, action: .updateChild1) + XCTAssertEqual(20, state.foo) // Parent is read only. + XCTAssertEqual(1, state.child1.foo) + XCTAssertEqual(1, state.child2.foo) + XCTAssertEqual(1, state.child3.foo) + } + + func testMultiChildComboWithWriteOnlyParent() { + struct MultiChildReducer: ReducerProtocol { + typealias State = ParentMultiChild + typealias Action = ParentMultiChildAction + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .updateChild1: + state.child1.foo += 1 + case .updateChild2: + state.child2.foo += 2 + case .updateChild3: + state.child3.foo += 3 + case .updateParent: + state.foo += 20 + } + return .none } - - let reducer = MultiChildReducer() - var state = ParentMultiChild( - foo: 0, - child1: .init(foo: 0), - child2: .init(foo: 0), - child3: .init(foo: 0) + .synchronizeState( + over: .init( + parent: .updateOnly(\ParentMultiChild.foo), + children: [ + .observeOnly(\ParentMultiChild.child1.foo), + .synchronize(\ParentMultiChild.child2.foo), + .updateOnly(\ParentMultiChild.child3.foo), + ] + ) ) - - // Update parent and nothing else changes becuase parent is write only. - _ = reducer.reduce(into: &state, action: .updateParent) - XCTAssertEqual(20, state.foo) - XCTAssertEqual(0, state.child1.foo) - XCTAssertEqual(0, state.child2.foo) - XCTAssertEqual(0, state.child3.foo) - - // Update child 3 and nothing changes with others, since it is not tracked for changes. - _ = reducer.reduce(into: &state, action: .updateChild3) - XCTAssertEqual(20, state.foo) - XCTAssertEqual(0, state.child1.foo) - XCTAssertEqual(0, state.child2.foo) - XCTAssertEqual(3, state.child3.foo) - - // Update child 1 and other writable ones change. - _ = reducer.reduce(into: &state, action: .updateChild1) - XCTAssertEqual(1, state.foo) // Parent is write only. - XCTAssertEqual(1, state.child1.foo) - XCTAssertEqual(1, state.child2.foo) - XCTAssertEqual(1, state.child3.foo) + } } + + let reducer = MultiChildReducer() + var state = ParentMultiChild( + foo: 0, + child1: .init(foo: 0), + child2: .init(foo: 0), + child3: .init(foo: 0) + ) + + // Update parent and nothing else changes becuase parent is write only. + _ = reducer.reduce(into: &state, action: .updateParent) + XCTAssertEqual(20, state.foo) + XCTAssertEqual(0, state.child1.foo) + XCTAssertEqual(0, state.child2.foo) + XCTAssertEqual(0, state.child3.foo) + + // Update child 3 and nothing changes with others, since it is not tracked for changes. + _ = reducer.reduce(into: &state, action: .updateChild3) + XCTAssertEqual(20, state.foo) + XCTAssertEqual(0, state.child1.foo) + XCTAssertEqual(0, state.child2.foo) + XCTAssertEqual(3, state.child3.foo) + + // Update child 1 and other writable ones change. + _ = reducer.reduce(into: &state, action: .updateChild1) + XCTAssertEqual(1, state.foo) // Parent is write only. + XCTAssertEqual(1, state.child1.foo) + XCTAssertEqual(1, state.child2.foo) + XCTAssertEqual(1, state.child3.foo) + } } struct Parent: Equatable { - var foo: Int - var child: Child + var foo: Int + var child: Child } enum ParentAction: Equatable { - case updateParent - case updateChild + case updateParent + case updateChild } enum ParentMultiChildAction: Equatable { - case updateParent - case updateChild1 - case updateChild2 - case updateChild3 + case updateParent + case updateChild1 + case updateChild2 + case updateChild3 } struct ParentMultiChild: Equatable { - var foo: Int - var child1: Child - var child2: Child - var child3: Child + var foo: Int + var child1: Child + var child2: Child + var child3: Child } struct Child: Equatable { - var foo: Int + var foo: Int }