diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Reducer.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Reducer.xcscheme index 492d6d3..46117df 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/Reducer.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/Reducer.xcscheme @@ -1,7 +1,7 @@ + LastUpgradeVersion = "1500" + version = "1.7"> @@ -20,6 +20,34 @@ ReferencedContainer = "container:"> + + + + + + + + + shouldAutocreateTestPlan = "YES"> + skipped = "NO"> + + + + This guide dose not cover the detailed principles of state design. > For more information, plrease refer to the [ReactorKit](https://github.com/ReactorKit/ReactorKit) or [TCA](https://github.com/pointfreeco/swift-composable-architecture) README. -To get started, you'll need to define a class that adopts the `Reduce` protocol. +To get started, You can define it via the `@Reduce` macro. or you can adopt the `Reduce` protocol. + +By default, `Reducer` runs on the UI's main thread, while `Reduce` does not. It's fine to use `Reduce` without the `@MainActor` constraint, but if you want the actions to run sequentially, add the `@MainActor` annotation. + +> ⚠️ When implementing `Reduce`, there is a slight difference in the use of macro and protocol. ```swift -final class CounterReduce: Reduce { +@Reduce +@MainActor +final class CounterReduce { // User interaction input. enum Action { case increase @@ -43,7 +49,7 @@ final class CounterReduce: Reduce { // Unit of state mutation. enum Mutation { - case addOne + case setCount(Int) } // Reducer state. @@ -51,17 +57,16 @@ final class CounterReduce: Reduce { var count: Int } - var mutator: (any Mutator)? - var initialState: State + let initialState: State - init(initialState: State) { - self.initialState = initialState + init() { + self.initialState = State() } - func mutate(state: State, action: Action) async throws { + func mutate(action: Action) async throws { switch action { case .increase: - mutate(.addOne) + mutate(.setCount(currentState.count + 1)) } } @@ -69,15 +74,15 @@ final class CounterReduce: Reduce { var state = state switch mutation { - case .addOne: - state.count += 1 + case let .setCount(count): + state.count = count return state } } } ``` -The `mutate(state:action) async throws` method defines what to mutate when an action received with the current state. You can call `mutate(_:)`(an extended function) to mutate. and `Swift Concurrency` can be used within the mutate method as well. +The `mutate(action:) async throws` method defines what to mutate when an action received with the current state. You can call `mutate(_:)`(an extended function) to mutate. and `Swift Concurrency` can be used within the mutate method as well. `reduce(state:mutation)` describe how to mutate the state from a mutation. It should be a pure function. @@ -135,10 +140,19 @@ You can cancel running action task using `shouldCancel(_:_:) -> Bool`. For example, if you want to cancel validating user input for each keystroke to efficiently use resources, `Reducer` can determine whether the current running task should be canceled before creating a new task action. If `shouldCancel(_:_:)` returns `true`, the current action should be canceled. +⚠️ The first thing to note about canceling a task is that the general expectation is that the comparison of actions should return `false` except for the case you want to cancel. + +The second thing to note that canceling a Task doesn't stop your code from progressing. In swift concurrency, [cancel](https://developer.apple.com/documentation/swift/task/cancel()) of task doesn't has no effect basically. + +If you want to make canceling a task meaningful, you'll need to [create a cancelable async method](https://developer.apple.com/documentation/swift/withtaskcancellationhandler(operation:oncancel:)) or utilize something like [`Task.checkCancellation()`](https://developer.apple.com/documentation/swift/task/checkcancellation()). + ```swift -final class SignUpReduce: Reduce { +@Reduce +@MainActor +final class SignUpReduce { enum Action { - case emailChanged(String) + case updateEmail(String) + case anyAction } enum Mutation { @@ -149,8 +163,7 @@ final class SignUpReduce: Reduce { var canSignUp: Bool } - var mutator: (any Mutator)? - var initialState: State + let initialState: State private let validator = EmailValidator() @@ -158,18 +171,28 @@ final class SignUpReduce: Reduce { initialState = State(canSignUp: false) } - func mutate(state: State, action: Action) async throws { + func mutate(action: Action) async throws { switch action { - case let .emailChanged(email): + case let .updateEmail(email): let result = try await validator.validate(email) + try Task.checkCancellation() + mutate(.canSignUp(result)) + + case .anyAction: + ... } } - func shouldCancel(_ current: ActionItem, _ upcoming: ActionItem) -> Bool { - switch (current.action, upcoming.action) { + ... + + func shouldCancel(_ current: Action, _ upcoming: Action) -> Bool { + switch (current, upcoming) { case (.emailChanged, .emailChanged): return true + + default: + return false } } } @@ -178,10 +201,12 @@ final class SignUpReduce: Reduce { ### Internal Mutating The reducer sometimes needs to mutate state without explicit outside action like some domain data changed. -In these case, you can use `start(with:)` function. It call once when `Reducer` set `Reduce`. So you can any initialize process with mutations. +In these case, you can use `start()` function. It call once when `Reducer` set `Reduce`. So you can any initialize process with mutations. ```swift -final class ListReduce: Reduce { +@Reduce +@MainActor +final class ListReduce { enum Action { ... } enum Mutation { @@ -194,20 +219,21 @@ final class ListReduce: Reduce { ... } - var mutator: (any Mutator)? - var initialState: State + let initialState: State + private var cancellableBag = Set() init() { ... } - func start(with mutator: any Mutator) async throws { + func start() async throws { + // Reset subscription when reduce re-start by reducer. + cancellableBag.removeAll() + NotificationCenter.default.publisher(for: .init("data_changed")) - .sink { data in - /* Write any mutates here. */ - mutator(.setList($0.object)) + .sink { [weak self] data in + // Write any mutates here. + self?.mutate(.setList(data.object)) } - // You can mutator scope cancellable bag. - // It all cancel when mutator(reducer) deinit. - .store(in: mutator.cancellableBag) + .store(in: &cancellableBag) } } ``` @@ -243,15 +269,10 @@ It maipulate all of `Reduce` even the `initialState`. ```swift CounterView(reducer: .init(proxy: .init( initialState: .init(count: 100), - mutate: { state, action, mutate in - - }, - reduce: { state, mutation in - // Return the state of result of mutating. - }, - shouldCancel: { current, upcoming in - // Return wether the current action should be canceled via the upcoming action. - } + start: { mutate in ... } + mutate: { state, action, mutate in ... }, + reduce: { state, mutation in ... }, + shouldCancel: { current, upcoming in ... } ))) ``` diff --git a/Sources/Reducer/Macro/ReduceMacro.swift b/Sources/Reducer/Macro/ReduceMacro.swift new file mode 100644 index 0000000..ef4c5b8 --- /dev/null +++ b/Sources/Reducer/Macro/ReduceMacro.swift @@ -0,0 +1,10 @@ +// +// ReduceMacro.swift +// +// +// Created by JSilver on 2023/06/09. +// + +@attached(member, names: named(mutator)) +@attached(extension, conformances: Reduce) +public macro Reduce() = #externalMacro(module: "ReducerMacro", type: "ReduceMacro") diff --git a/Sources/Reducer/Mutable.swift b/Sources/Reducer/Mutable.swift index 6a80589..ed3f759 100644 --- a/Sources/Reducer/Mutable.swift +++ b/Sources/Reducer/Mutable.swift @@ -12,11 +12,8 @@ public protocol Mutable: AnyObject { associatedtype Mutation associatedtype State - var initialState: State { get } var state: State { get } - var cancellableBag: Set { get set } - func mutate(_ mutation: Mutation) } @@ -28,34 +25,21 @@ public extension Mutable { open class Mutator: Mutable { // MARK: - Propery - public let initialState: State - private let _state: () -> State public var state: State { _state() } - private let _cancellableBag: UnsafeMutablePointer> - public var cancellableBag: Set { - get { _cancellableBag.pointee } - set { _cancellableBag.pointee = newValue } - } - - private let _mutate: ((Mutation) -> Void)? + private let _mutate: (Mutation) -> Void // MARK: - Initializer - public init(_ mutator: M) where M.Mutation == Mutation, M.State == State { - let initialState = mutator.initialState - self.initialState = initialState - + public init(_ mutator: M, initialState: State) where M.Mutation == Mutation, M.State == State { self._state = { [weak mutator] in mutator?.state ?? initialState } - self._cancellableBag = withUnsafeMutablePointer(to: &mutator.cancellableBag) { $0 } - self._mutate = { [weak mutator] mutation in mutator?.mutate(mutation)} } // MARK: - Lifecycle open func mutate(_ mutation: Mutation) { - _mutate?(mutation) + _mutate(mutation) } // MARK: - Public diff --git a/Sources/Reducer/Reduce.swift b/Sources/Reducer/Reduce.swift index 3dc2da8..112feca 100644 --- a/Sources/Reducer/Reduce.swift +++ b/Sources/Reducer/Reduce.swift @@ -13,26 +13,23 @@ public protocol Reduce: AnyObject { associatedtype Mutation associatedtype State - typealias ActionItem = (state: State, action: Action) - var mutator: Mutator? { get set } var initialState: State { get } - func start(with mutator: Mutator) async throws - - func mutate(state: State, action: Action) async throws + func start() async throws + + func mutate(action: Action) async throws func reduce(state: State, mutation: Mutation) -> State - func shouldCancel(_ current: ActionItem, _ upcoming: ActionItem) -> Bool + + func shouldCancel(_ current: Action, _ upcoming: Action) -> Bool } public extension Reduce { var currentState: State { mutator?.state ?? initialState } - func start(with mutator: Mutator) async throws { - - } + func start() async throws { } - func shouldCancel(_ current: ActionItem, _ upcoming: ActionItem) -> Bool { + func shouldCancel(_ current: Action, _ upcoming: Action) -> Bool { false } @@ -66,21 +63,31 @@ open class ProxyReduce: Reduce { /// ``` /// /// To fix this, replace the constrainted existential type with a generic class. - public var mutator: Mutator? + private var _mutator: UnsafeMutablePointer?> = .allocate(capacity: 1) + public var mutator: Mutator? { + get { + _mutator.pointee + } + set { + _mutator.pointee = newValue + } + } + private var isMutatorAllocated: Bool = true + public var initialState: State - private let _start: ((Mutator) async throws -> Void)? - private let _mutate: ((State, Action, Mutator) async throws -> Void)? + private let _start: ((@escaping (Mutation) -> Void) async throws -> Void)? + private let _mutate: ((State, Action, @escaping (Mutation) -> Void) async throws -> Void)? private let _reduce: ((State, Mutation) -> State)? - private let _shouldCancel: ((ActionItem, ActionItem) -> Bool)? + private let _shouldCancel: ((Action, Action) -> Bool)? // MARK: - Initalizer - public init( + public init( initialState: State, - start: ((Mutator) async throws -> Void)? = nil, - mutate: ((State, Action, Mutator) async throws -> Void)? = nil, + start: ((@escaping (Mutation) -> Void) async throws -> Void)? = nil, + mutate: ((State, Action, @escaping (Mutation) -> Void) async throws -> Void)? = nil, reduce: ((State, Mutation) -> State)? = nil, - shouldCancel: (((state: State, action: Action), (state: State, action: Action)) -> Bool)? = nil + shouldCancel: ((Action, Action) -> Bool)? = nil ) { self.initialState = initialState @@ -93,12 +100,11 @@ open class ProxyReduce: Reduce { convenience init(_ reduce: R) { self.init( initialState: reduce.initialState, - start: { mutator in - reduce.mutator = mutator - try await reduce.start(with: mutator) + start: { _ in + try await reduce.start() }, - mutate: { state, action, _ in - try await reduce.mutate(state: state, action: action) + mutate: { _, action, _ in + try await reduce.mutate(action: action) }, reduce: { state, mutation in reduce.reduce(state: state, mutation: mutation) @@ -107,28 +113,39 @@ open class ProxyReduce: Reduce { reduce.shouldCancel(current, upcoming) } ) + + // Set flag to false to indicate mutator pointer deallocated. + _mutator.deallocate() + isMutatorAllocated = false + + _mutator = withUnsafeMutablePointer(to: &reduce.mutator) { $0 } } // MARK: - Lifecycle - open func start(with mutator: Mutator) async throws { - self.mutator = mutator - try await _start?(mutator) + open func start() async throws { + try await _start?({ [weak self] mutation in self?.mutator?(mutation) }) } - open func mutate(state: State, action: Action) async throws { - guard let mutator else { return } - try await _mutate?(state, action, mutator) + open func mutate(action: Action) async throws { + try await _mutate?(currentState, action, { [weak self] mutation in self?.mutator?(mutation) }) } open func reduce(state: State, mutation: Mutation) -> State { _reduce?(state, mutation) ?? state } - open func shouldCancel(_ current: ActionItem, _ upcoming: ActionItem) -> Bool { + open func shouldCancel(_ current: Action, _ upcoming: Action) -> Bool { _shouldCancel?(current, upcoming) ?? false } // MARK: - Public // MARK: - Private + + deinit { + if isMutatorAllocated { + // Deallocate pointer when mutator allocated. + _mutator.deallocate() + } + } } diff --git a/Sources/Reducer/Reducer.swift b/Sources/Reducer/Reducer.swift index 47474af..39db564 100644 --- a/Sources/Reducer/Reducer.swift +++ b/Sources/Reducer/Reducer.swift @@ -8,15 +8,14 @@ import Foundation import Combine -@MainActor final class TaskBag { struct TaskItem: Hashable { // MARK: - Property let item: Item - let task: Task + let task: Task // MARK: - Initalizer - init(_ item: Item, with task: Task) { + init(_ item: Item, with task: Task) { self.item = item self.task = task } @@ -46,9 +45,9 @@ final class TaskBag { func store(_ item: TaskItem) { items.insert(item) - Task { [weak self] in + Task { await item.task.value - self?.remove(item) + items.remove(item) } } @@ -56,10 +55,6 @@ final class TaskBag { try items.forEach(body) } - func remove(_ item: TaskItem) { - items.remove(item) - } - // MARK: - Private deinit { @@ -75,23 +70,20 @@ open class Reducer: ObservableObject, Mutable { // MARK: - Property @Published public private(set) var state: State - public let initialState: State private let reduce: ProxyReduce - public var cancellableBag = Set() - private let taskBag = TaskBag() + private let taskBag = TaskBag() // MARK: - Initalizer public init(proxy reduce: ProxyReduce) { - self.reduce = reduce - self.state = reduce.initialState - self.initialState = reduce.initialState + self.reduce = reduce - // Start reduce with mutator. + reduce.mutator = Mutator(self, initialState: reduce.initialState) Task { - try await self.reduce.start(with: Mutator(self)) + // Start reduce with mutator. + try? await reduce.start() } } @@ -100,39 +92,28 @@ open class Reducer: ObservableObject, Mutable { } // MARK: - Lifecycle - public func mutate(_ mutation: Mutation) { - Task { - // Reduce state from mutation. - // Run task on main actor context. - reduce(mutation: mutation) - } + open func mutate(_ mutation: Mutation) { + // Reduce state from mutation. + state = reduce(state: state, mutation: mutation) } // MARK: - Public - public func action(_ action: Action) { + open func action(_ action: Action) { let reduce = reduce - let state = state - taskBag.forEach { - guard reduce.shouldCancel($0.item, (state, action)) else { return } - $0.cancel() + // Traversing the task and deciding to cancel it. + taskBag.forEach { task in + guard reduce.shouldCancel(task.item, action) else { return } + task.cancel() } // Store task into bag. taskBag.store(.init( - (state, action), - with: Task { + action, + with: Task { @MainActor in // Mutate state from action. - try? await reduce.mutate( - state: state, - action: action - ) + try? await reduce.mutate(action: action) } )) } - - // MARK: - Private - private func reduce(mutation: Mutation) { - state = reduce(state: state, mutation: mutation) - } } diff --git a/Sources/ReducerMacro/Plugin.swift b/Sources/ReducerMacro/Plugin.swift new file mode 100644 index 0000000..1bb2413 --- /dev/null +++ b/Sources/ReducerMacro/Plugin.swift @@ -0,0 +1,16 @@ +// +// Plugin.swift +// +// +// Created by JSilver on 2023/06/09. +// + +import SwiftCompilerPlugin +import SwiftSyntaxMacros + +@main +struct Plugin: CompilerPlugin { + let providingMacros: [Macro.Type] = [ + ReduceMacro.self + ] +} diff --git a/Sources/ReducerMacro/ReduceMacro.swift b/Sources/ReducerMacro/ReduceMacro.swift new file mode 100644 index 0000000..f884480 --- /dev/null +++ b/Sources/ReducerMacro/ReduceMacro.swift @@ -0,0 +1,45 @@ +// +// ReduceMacro.swift +// +// +// Created by JSilver on 2023/06/09. +// + +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +public struct ReduceMacro { } + +extension ReduceMacro: ExtensionMacro { + public static func expansion( + of node: SwiftSyntax.AttributeSyntax, + attachedTo declaration: some SwiftSyntax.DeclGroupSyntax, + providingExtensionsOf type: some SwiftSyntax.TypeSyntaxProtocol, + conformingTo protocols: [SwiftSyntax.TypeSyntax], + in context: some SwiftSyntaxMacros.MacroExpansionContext + ) throws -> [SwiftSyntax.ExtensionDeclSyntax] { + let extenstion = DeclSyntax(""" + extension \(type.trimmed): Reduce { } + """) + + return [extenstion.cast(ExtensionDeclSyntax.self)] + } +} + +extension ReduceMacro: MemberMacro { + public static func expansion< + Declaration: DeclGroupSyntax, + Context: MacroExpansionContext + >( + of node: AttributeSyntax, + providingMembersOf declaration: Declaration, + in context: Context + ) throws -> [DeclSyntax] { + let mutatorSyntax = DeclSyntax(""" + var mutator: Mutator? + """) + + return [mutatorSyntax] + } +} diff --git a/Tests/MacroTests/ReduceMacroTests.swift b/Tests/MacroTests/ReduceMacroTests.swift new file mode 100644 index 0000000..8fd5791 --- /dev/null +++ b/Tests/MacroTests/ReduceMacroTests.swift @@ -0,0 +1,52 @@ +// +// ReduceMacroTests.swift +// +// +// Created by JSilver on 2023/06/09. +// + +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import XCTest +import ReducerMacro + +let testMacros: [String: Macro.Type] = [ + "Reduce": ReduceMacro.self, +] + +@MainActor +final class ReduceMacroTests: XCTestCase { + // MARK: - Property + + // MARK: - Lifecycle + override func setUp() { + + } + + override func tearDown() { + + } + + // MARK: - Test + func test_that_reduce_macro_expand_source() { + assertMacroExpansion( + """ + @Reduce + class Test { + + } + """, + expandedSource: """ + class Test { + + var mutator: Mutator? + + } + + extension Test: Reduce { + } + """, + macros: testMacros + ) + } +} diff --git a/Tests/ReducerTests/Model/AwaitStartReduce.swift b/Tests/ReducerTests/Model/AwaitStartReduce.swift index 4012203..aaf30a4 100644 --- a/Tests/ReducerTests/Model/AwaitStartReduce.swift +++ b/Tests/ReducerTests/Model/AwaitStartReduce.swift @@ -8,7 +8,9 @@ import Foundation import Reducer -class AwaitStartReduce: Reduce { +@Reduce +@MainActor +class AwaitStartReduce { enum Action { case empty } @@ -22,8 +24,7 @@ class AwaitStartReduce: Reduce { } // MARK: - Property - var mutator: Mutator? - var initialState: State + let initialState: State // MARK: - Initializer init(initialState: State) { @@ -31,12 +32,12 @@ class AwaitStartReduce: Reduce { } // MARK: - Lifecycle - func start(with mutator: Mutator) async throws { + func start() async throws { try await Task.sleep(nanoseconds: 10_000_000) - mutator.mutate(.increase) + mutate(.increase) } - func mutate(state: State, action: Action) async throws { + func mutate(action: Action) async throws { } diff --git a/Tests/ReducerTests/Model/CountIncreaseReduce.swift b/Tests/ReducerTests/Model/CountIncreaseReduce.swift index e242ec8..6c96e4e 100644 --- a/Tests/ReducerTests/Model/CountIncreaseReduce.swift +++ b/Tests/ReducerTests/Model/CountIncreaseReduce.swift @@ -7,7 +7,9 @@ import Reducer -class CountIncreaseReduce: Reduce { +@Reduce +@MainActor +class CountIncreaseReduce { enum Action { case increase } @@ -21,8 +23,7 @@ class CountIncreaseReduce: Reduce { } // MARK: - Property - var mutator: Mutator? - var initialState: State + let initialState: State // MARK: - Initializer init(initialState: State) { @@ -30,7 +31,7 @@ class CountIncreaseReduce: Reduce { } // MARK: - Lifecycle - func mutate(state: State, action: Action) async throws { + func mutate(action: Action) async throws { switch action { case .increase: // Increase count after waiting 0.1 sec. diff --git a/Tests/ReducerTests/Model/CountSetReduce.swift b/Tests/ReducerTests/Model/CountSetReduce.swift index 613b284..a2025a5 100644 --- a/Tests/ReducerTests/Model/CountSetReduce.swift +++ b/Tests/ReducerTests/Model/CountSetReduce.swift @@ -7,7 +7,9 @@ import Reducer -class CountSetReduce: Reduce { +@Reduce +@MainActor +class CountSetReduce { enum Action { case increase } @@ -21,8 +23,7 @@ class CountSetReduce: Reduce { } // MARK: - Property - var mutator: Mutator? - var initialState: State + let initialState: State // MARK: - Initializer init(initialState: State) { @@ -30,12 +31,12 @@ class CountSetReduce: Reduce { } // MARK: - Lifecycle - func mutate(state: State, action: Action) async throws { + func mutate(action: Action) async throws { switch action { case .increase: // Increase count after waiting 0.1 sec. try await Task.sleep(nanoseconds: 10_000_000) - mutate(.setCount(state.count + 1)) + mutate(.setCount(currentState.count + 1)) } } @@ -49,8 +50,8 @@ class CountSetReduce: Reduce { } } - func shouldCancel(_ current: ActionItem, _ upcoming: ActionItem) -> Bool { + func shouldCancel(_ current: Action, _ upcoming: Action) -> Bool { // Cancel previous action when same action occured. - current.action == upcoming.action + current == upcoming } } diff --git a/Tests/ReducerTests/Model/ProxyCountIncrease10Reduce.swift b/Tests/ReducerTests/Model/ProxyCountIncrease10Reduce.swift index 1ecffcc..382ec13 100644 --- a/Tests/ReducerTests/Model/ProxyCountIncrease10Reduce.swift +++ b/Tests/ReducerTests/Model/ProxyCountIncrease10Reduce.swift @@ -8,7 +8,7 @@ import Reducer class ProxyCountIncrease10Reduce: ProxyReduce { - override func mutate(state: State, action: Action) async throws { + override func mutate(action: Action) async throws { switch action { case .increase: try await Task.sleep(nanoseconds: 100_000_000) diff --git a/Tests/ReducerTests/Model/TimerReduce.swift b/Tests/ReducerTests/Model/TimerReduce.swift index 1594296..e97c17e 100644 --- a/Tests/ReducerTests/Model/TimerReduce.swift +++ b/Tests/ReducerTests/Model/TimerReduce.swift @@ -7,8 +7,11 @@ import Foundation import Reducer +import Combine -class TimerReduce: Reduce { +@Reduce +@MainActor +class TimerReduce { enum Action { case empty } @@ -22,8 +25,9 @@ class TimerReduce: Reduce { } // MARK: - Property - var mutator: Mutator? - var initialState: State + let initialState: State + + private var cancellableBag = Set() // MARK: - Initializer init(initialState: State) { @@ -31,14 +35,16 @@ class TimerReduce: Reduce { } // MARK: - Lifecycle - func start(with mutator: Mutator) async throws { + func start() async throws { + cancellableBag.removeAll() + Timer.publish(every: 0.1, on: .main, in: .default) .autoconnect() - .sink { _ in mutator(.increase) } - .store(in: &mutator.cancellableBag) + .sink { [weak self] _ in self?.mutate(.increase) } + .store(in: &cancellableBag) } - func mutate(state: State, action: Action) async throws { + func mutate(action: Action) async throws { } diff --git a/Tests/ReducerTests/ReducerTests.swift b/Tests/ReducerTests/ReducerTests.swift index 7aa8fb7..199e424 100644 --- a/Tests/ReducerTests/ReducerTests.swift +++ b/Tests/ReducerTests/ReducerTests.swift @@ -278,7 +278,7 @@ final class ReducerTests: XCTestCase { } }, shouldCancel: { current, upcoming in - current.action == upcoming.action + current == upcoming } )) @@ -298,13 +298,13 @@ final class ReducerTests: XCTestCase { func test_that_count_increases_when_proxy_mutate_in_start() async throws { // Given + var cancellable: AnyCancellable? = nil let reducer = Reducer(proxy: .init( initialState: .init(count: 0), - start: { mutator in - Timer.publish(every: 0.1, on: .main, in: .default) + start: { mutate in + cancellable = Timer.publish(every: 0.1, on: .main, in: .default) .autoconnect() - .sink { _ in mutator(.increase) } - .store(in: &mutator.cancellableBag) + .sink { _ in mutate(.increase) } }, reduce: { state, mutation in var state = state