From 59570ec68197f4e865d8147a1b955eeaed79250c Mon Sep 17 00:00:00 2001 From: Jeong Jineun Date: Fri, 9 Jun 2023 20:39:51 +0900 Subject: [PATCH 01/11] feat(): add @Reduce macro --- .../xcshareddata/xcschemes/Reducer.xcscheme | 16 ++++-- Package.resolved | 14 ++++++ Package.swift | 21 ++++++-- Sources/Macro/Plugin.swift | 16 ++++++ Sources/Macro/ReduceMacro.swift | 40 +++++++++++++++ Sources/Reducer/Macro/ReduceMacro.swift | 10 ++++ Tests/MacroTests/ReduceMacroTests.swift | 49 +++++++++++++++++++ 7 files changed, 159 insertions(+), 7 deletions(-) create mode 100644 Package.resolved create mode 100644 Sources/Macro/Plugin.swift create mode 100644 Sources/Macro/ReduceMacro.swift create mode 100644 Sources/Reducer/Macro/ReduceMacro.swift create mode 100644 Tests/MacroTests/ReduceMacroTests.swift diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Reducer.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Reducer.xcscheme index 492d6d3..1ecfec0 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/Reducer.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/Reducer.xcscheme @@ -26,12 +26,10 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES" - codeCoverageEnabled = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES"> + skipped = "NO"> + + + + ( + of node: AttributeSyntax, + providingConformancesOf declaration: Declaration, + in context: Context + ) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)] { + return [("Reduce", nil)] + } + + 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/Sources/Reducer/Macro/ReduceMacro.swift b/Sources/Reducer/Macro/ReduceMacro.swift new file mode 100644 index 0000000..29332c6 --- /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(conformance) +public macro Reduce() = #externalMacro(module: "Macro", type: "ReduceMacro") diff --git a/Tests/MacroTests/ReduceMacroTests.swift b/Tests/MacroTests/ReduceMacroTests.swift new file mode 100644 index 0000000..a065e22 --- /dev/null +++ b/Tests/MacroTests/ReduceMacroTests.swift @@ -0,0 +1,49 @@ +// +// ReduceMacroTests.swift +// +// +// Created by JSilver on 2023/06/09. +// + +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import XCTest +import Macro + +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? + + } + """, + macros: testMacros + ) + } +} From 0e5f1ee0a3c06f9e31eea7a084a4e32fe36c7921 Mon Sep 17 00:00:00 2001 From: Jeong Jineun Date: Thu, 10 Aug 2023 14:39:21 +0900 Subject: [PATCH 02/11] fix(): fix deprecated conformance macro to extension macro --- Sources/Macro/ReduceMacro.swift | 23 +++++++++++++---------- Sources/Reducer/Macro/ReduceMacro.swift | 2 +- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/Sources/Macro/ReduceMacro.swift b/Sources/Macro/ReduceMacro.swift index 5a049c6..a7b795c 100644 --- a/Sources/Macro/ReduceMacro.swift +++ b/Sources/Macro/ReduceMacro.swift @@ -9,16 +9,19 @@ import SwiftSyntax import SwiftSyntaxBuilder import SwiftSyntaxMacros -public struct ReduceMacro: ConformanceMacro, MemberMacro { - public static func expansion< - Declaration: DeclGroupSyntax, - Context: MacroExpansionContext - >( - of node: AttributeSyntax, - providingConformancesOf declaration: Declaration, - in context: Context - ) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)] { - return [("Reduce", nil)] +public struct ReduceMacro: ExtensionMacro, MemberMacro { + 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): Equatable { } + """ + + return [extenstion.cast(ExtensionDeclSyntax.self)] } public static func expansion< diff --git a/Sources/Reducer/Macro/ReduceMacro.swift b/Sources/Reducer/Macro/ReduceMacro.swift index 29332c6..cdafbd7 100644 --- a/Sources/Reducer/Macro/ReduceMacro.swift +++ b/Sources/Reducer/Macro/ReduceMacro.swift @@ -6,5 +6,5 @@ // @attached(member, names: named(mutator)) -@attached(conformance) +@attached(extension) public macro Reduce() = #externalMacro(module: "Macro", type: "ReduceMacro") From 016d886eac01137f375972c26dc67095eb1a409b Mon Sep 17 00:00:00 2001 From: Jeong Jineun Date: Fri, 11 Aug 2023 15:13:30 +0900 Subject: [PATCH 03/11] fix(): fix extension macro to add conformanced protocol - update swift-syntax package version --- Package.swift | 2 +- Sources/Macro/ReduceMacro.swift | 4 ++-- Sources/Reducer/Macro/ReduceMacro.swift | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Package.swift b/Package.swift index 0dc5953..b5f13f2 100644 --- a/Package.swift +++ b/Package.swift @@ -20,7 +20,7 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0-swift-5.9-DEVELOPMENT-SNAPSHOT-2023-04-25-b") + .package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0-swift-DEVELOPMENT-SNAPSHOT-2023-08-07-a") ], targets: [ .macro( diff --git a/Sources/Macro/ReduceMacro.swift b/Sources/Macro/ReduceMacro.swift index a7b795c..14ba8fd 100644 --- a/Sources/Macro/ReduceMacro.swift +++ b/Sources/Macro/ReduceMacro.swift @@ -18,8 +18,8 @@ public struct ReduceMacro: ExtensionMacro, MemberMacro { in context: some SwiftSyntaxMacros.MacroExpansionContext ) throws -> [SwiftSyntax.ExtensionDeclSyntax] { let extenstion: DeclSyntax = """ - extension \(type.trimmed): Equatable { } - """ + extension \(type.trimmed): Reduce { } + """ return [extenstion.cast(ExtensionDeclSyntax.self)] } diff --git a/Sources/Reducer/Macro/ReduceMacro.swift b/Sources/Reducer/Macro/ReduceMacro.swift index cdafbd7..759be5b 100644 --- a/Sources/Reducer/Macro/ReduceMacro.swift +++ b/Sources/Reducer/Macro/ReduceMacro.swift @@ -6,5 +6,5 @@ // @attached(member, names: named(mutator)) -@attached(extension) +@attached(extension, conformances: Reduce) public macro Reduce() = #externalMacro(module: "Macro", type: "ReduceMacro") From 0d6ebfd2edb6c69eddabed323ad40310d41c2f89 Mon Sep 17 00:00:00 2001 From: Jeong Jineun Date: Sat, 12 Aug 2023 16:16:54 +0900 Subject: [PATCH 04/11] feat(): rename macro target to avoid duplication --- .../xcshareddata/xcschemes/Reducer.xcscheme | 47 +++++++++++++++---- Package.resolved | 4 +- Package.swift | 6 +-- Sources/Reducer/Macro/ReduceMacro.swift | 2 +- Sources/{Macro => ReducerMacro}/Plugin.swift | 0 .../{Macro => ReducerMacro}/ReduceMacro.swift | 16 ++++--- Tests/MacroTests/ReduceMacroTests.swift | 9 ++-- 7 files changed, 59 insertions(+), 25 deletions(-) rename Sources/{Macro => ReducerMacro}/Plugin.swift (100%) rename Sources/{Macro => ReducerMacro}/ReduceMacro.swift (84%) diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Reducer.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Reducer.xcscheme index 1ecfec0..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,21 +20,50 @@ ReferencedContainer = "container:"> + + + + + + + + + shouldUseLaunchSchemeArgsEnv = "YES" + shouldAutocreateTestPlan = "YES"> @@ -42,9 +71,9 @@ skipped = "NO"> diff --git a/Package.resolved b/Package.resolved index c4f9794..9e3d48a 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-syntax.git", "state" : { - "revision" : "165fc6d22394c1168ff76ab5d951245971ef07e5", - "version" : "509.0.0-swift-DEVELOPMENT-SNAPSHOT-2023-06-05-a" + "revision" : "e42bece52df2496d60b1b2a762fee9ffde7fc205", + "version" : "509.0.0-swift-DEVELOPMENT-SNAPSHOT-2023-08-07-a" } } ], diff --git a/Package.swift b/Package.swift index b5f13f2..597e184 100644 --- a/Package.swift +++ b/Package.swift @@ -24,7 +24,7 @@ let package = Package( ], targets: [ .macro( - name: "Macro", + name: "ReducerMacro", dependencies: [ .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), .product(name: "SwiftCompilerPlugin", package: "swift-syntax") @@ -33,7 +33,7 @@ let package = Package( .target( name: "Reducer", dependencies: [ - "Macro" + "ReducerMacro" ] ), .testTarget( @@ -45,7 +45,7 @@ let package = Package( .testTarget( name: "MacroTests", dependencies: [ - "Macro", + "ReducerMacro", .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), ] ) diff --git a/Sources/Reducer/Macro/ReduceMacro.swift b/Sources/Reducer/Macro/ReduceMacro.swift index 759be5b..ef4c5b8 100644 --- a/Sources/Reducer/Macro/ReduceMacro.swift +++ b/Sources/Reducer/Macro/ReduceMacro.swift @@ -7,4 +7,4 @@ @attached(member, names: named(mutator)) @attached(extension, conformances: Reduce) -public macro Reduce() = #externalMacro(module: "Macro", type: "ReduceMacro") +public macro Reduce() = #externalMacro(module: "ReducerMacro", type: "ReduceMacro") diff --git a/Sources/Macro/Plugin.swift b/Sources/ReducerMacro/Plugin.swift similarity index 100% rename from Sources/Macro/Plugin.swift rename to Sources/ReducerMacro/Plugin.swift diff --git a/Sources/Macro/ReduceMacro.swift b/Sources/ReducerMacro/ReduceMacro.swift similarity index 84% rename from Sources/Macro/ReduceMacro.swift rename to Sources/ReducerMacro/ReduceMacro.swift index 14ba8fd..f884480 100644 --- a/Sources/Macro/ReduceMacro.swift +++ b/Sources/ReducerMacro/ReduceMacro.swift @@ -9,7 +9,9 @@ import SwiftSyntax import SwiftSyntaxBuilder import SwiftSyntaxMacros -public struct ReduceMacro: ExtensionMacro, MemberMacro { +public struct ReduceMacro { } + +extension ReduceMacro: ExtensionMacro { public static func expansion( of node: SwiftSyntax.AttributeSyntax, attachedTo declaration: some SwiftSyntax.DeclGroupSyntax, @@ -17,13 +19,15 @@ public struct ReduceMacro: ExtensionMacro, MemberMacro { conformingTo protocols: [SwiftSyntax.TypeSyntax], in context: some SwiftSyntaxMacros.MacroExpansionContext ) throws -> [SwiftSyntax.ExtensionDeclSyntax] { - let extenstion: DeclSyntax = """ + let extenstion = DeclSyntax(""" extension \(type.trimmed): Reduce { } - """ + """) return [extenstion.cast(ExtensionDeclSyntax.self)] } - +} + +extension ReduceMacro: MemberMacro { public static func expansion< Declaration: DeclGroupSyntax, Context: MacroExpansionContext @@ -36,8 +40,6 @@ public struct ReduceMacro: ExtensionMacro, MemberMacro { var mutator: Mutator? """) - return [ - mutatorSyntax - ] + return [mutatorSyntax] } } diff --git a/Tests/MacroTests/ReduceMacroTests.swift b/Tests/MacroTests/ReduceMacroTests.swift index a065e22..8fd5791 100644 --- a/Tests/MacroTests/ReduceMacroTests.swift +++ b/Tests/MacroTests/ReduceMacroTests.swift @@ -8,7 +8,7 @@ import SwiftSyntaxMacros import SwiftSyntaxMacrosTestSupport import XCTest -import Macro +import ReducerMacro let testMacros: [String: Macro.Type] = [ "Reduce": ReduceMacro.self, @@ -37,10 +37,13 @@ final class ReduceMacroTests: XCTestCase { } """, expandedSource: """ - class Test { + var mutator: Mutator? - + + } + + extension Test: Reduce { } """, macros: testMacros From c9294cdcaa5a937b43852a121fcabab7b8ef5495 Mon Sep 17 00:00:00 2001 From: Jeong Jineun Date: Sat, 12 Aug 2023 16:40:15 +0900 Subject: [PATCH 05/11] feat(): update README.md about cancel of task --- README.md | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index da5a444..bc22e91 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ ## Swift Package Manager ```swift dependencies: [ - .package(url: "https://github.com/wlsdms0122/Reducer.git", exact: "1.1.0") + .package(url: "https://github.com/wlsdms0122/Reducer.git", exact: "1.4.0") ] ``` @@ -51,7 +51,7 @@ final class CounterReduce: Reduce { var count: Int } - var mutator: (any Mutator)? + var mutator: Mutator? var initialState: State init(initialState: State) { @@ -135,10 +135,17 @@ 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 { enum Action { case emailChanged(String) + case anyAction } enum Mutation { @@ -149,7 +156,7 @@ final class SignUpReduce: Reduce { var canSignUp: Bool } - var mutator: (any Mutator)? + var mutator: Mutator? var initialState: State private let validator = EmailValidator() @@ -162,14 +169,24 @@ final class SignUpReduce: Reduce { switch action { case let .emailChanged(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) { case (.emailChanged, .emailChanged): return true + + default: + return false } } } @@ -202,7 +219,7 @@ final class ListReduce: Reduce { func start(with mutator: any Mutator) async throws { NotificationCenter.default.publisher(for: .init("data_changed")) .sink { data in - /* Write any mutates here. */ + // Write any mutates here. mutator(.setList($0.object)) } // You can mutator scope cancellable bag. From c22e67a946ae7d1e6931e3000b67dcf4518da520 Mon Sep 17 00:00:00 2001 From: Jeong Jineun Date: Sat, 9 Sep 2023 15:13:28 +0900 Subject: [PATCH 06/11] fix(): fix typo of README --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index da5a444..0fbd8f6 100644 --- a/README.md +++ b/README.md @@ -51,8 +51,8 @@ final class CounterReduce: Reduce { var count: Int } - var mutator: (any Mutator)? - var initialState: State + var mutator: Mutator? + let initialState: State init(initialState: State) { self.initialState = initialState @@ -138,7 +138,7 @@ For example, if you want to cancel validating user input for each keystroke to e ```swift final class SignUpReduce: Reduce { enum Action { - case emailChanged(String) + case updateEmail(String) } enum Mutation { @@ -150,7 +150,7 @@ final class SignUpReduce: Reduce { } var mutator: (any Mutator)? - var initialState: State + let initialState: State private let validator = EmailValidator() @@ -160,7 +160,7 @@ final class SignUpReduce: Reduce { func mutate(state: State, action: Action) async throws { switch action { - case let .emailChanged(email): + case let .updateEmail(email): let result = try await validator.validate(email) mutate(.canSignUp(result)) } @@ -195,14 +195,14 @@ final class ListReduce: Reduce { } var mutator: (any Mutator)? - var initialState: State + let initialState: State init() { ... } func start(with mutator: any Mutator) async throws { NotificationCenter.default.publisher(for: .init("data_changed")) .sink { data in - /* Write any mutates here. */ + // Write any mutates here. mutator(.setList($0.object)) } // You can mutator scope cancellable bag. From df151c3c74d9b7aac8f29c9c554b70df4ba06bbc Mon Sep 17 00:00:00 2001 From: Jeong Jineun Date: Tue, 19 Sep 2023 21:33:33 +0900 Subject: [PATCH 07/11] feat(): update README about `@Reduce` macro --- README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index da5a444..8116ce7 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,8 @@ dependencies: [ To get started, you'll need to define a class that adopts the `Reduce` protocol. ```swift -final class CounterReduce: Reduce { +@Reduce +final class CounterReduce { // User interaction input. enum Action { case increase @@ -51,11 +52,10 @@ 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 { @@ -136,7 +136,8 @@ 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. ```swift -final class SignUpReduce: Reduce { +@Reduce +final class SignUpReduce { enum Action { case emailChanged(String) } @@ -149,8 +150,7 @@ final class SignUpReduce: Reduce { var canSignUp: Bool } - var mutator: (any Mutator)? - var initialState: State + let initialState: State private let validator = EmailValidator() @@ -181,7 +181,8 @@ The reducer sometimes needs to mutate state without explicit outside action like In these case, you can use `start(with:)` function. It call once when `Reducer` set `Reduce`. So you can any initialize process with mutations. ```swift -final class ListReduce: Reduce { +@Reduce +final class ListReduce { enum Action { ... } enum Mutation { @@ -194,8 +195,7 @@ final class ListReduce: Reduce { ... } - var mutator: (any Mutator)? - var initialState: State + let initialState: State init() { ... } From 6a240e2db0c228219258fea788d48fc5f50b281f Mon Sep 17 00:00:00 2001 From: Jeong Jineun Date: Sun, 24 Sep 2023 16:23:15 +0900 Subject: [PATCH 08/11] feat(): update `swift-syntax` package version to 509.0.0 due to release --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 597e184..121c585 100644 --- a/Package.swift +++ b/Package.swift @@ -20,7 +20,7 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0-swift-DEVELOPMENT-SNAPSHOT-2023-08-07-a") + .package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0") ], targets: [ .macro( From c4d247cb20e63a024419a1175e47660389a32ae6 Mon Sep 17 00:00:00 2001 From: Jeong Jineun Date: Sun, 24 Sep 2023 17:47:54 +0900 Subject: [PATCH 09/11] feat(): update `Reducer`'s interfaces - update test cases to apply changes --- Sources/Reducer/Mutable.swift | 22 +----- Sources/Reducer/Reduce.swift | 75 +++++++++++-------- Sources/Reducer/Reducer.swift | 54 +++++-------- .../ReducerTests/Model/AwaitStartReduce.swift | 13 ++-- .../Model/CountIncreaseReduce.swift | 9 ++- Tests/ReducerTests/Model/CountSetReduce.swift | 15 ++-- .../Model/ProxyCountIncrease10Reduce.swift | 2 +- Tests/ReducerTests/Model/TimerReduce.swift | 20 +++-- Tests/ReducerTests/ReducerTests.swift | 10 +-- 9 files changed, 105 insertions(+), 115 deletions(-) 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..2d1a4e0 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,37 @@ open class ProxyReduce: Reduce { reduce.shouldCancel(current, upcoming) } ) + + _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 { + _mutator.deallocate() + } + } } diff --git a/Sources/Reducer/Reducer.swift b/Sources/Reducer/Reducer.swift index 47474af..7eeac0e 100644 --- a/Sources/Reducer/Reducer.swift +++ b/Sources/Reducer/Reducer.swift @@ -13,10 +13,10 @@ 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 +46,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 +56,6 @@ final class TaskBag { try items.forEach(body) } - func remove(_ item: TaskItem) { - items.remove(item) - } - // MARK: - Private deinit { @@ -75,23 +71,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)) + try? await reduce.start() } } @@ -100,39 +93,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. + // Run task on main actor context. + 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() + taskBag.forEach { task in + guard reduce.shouldCancel(task.item, action) else { return } + task.cancel() } // Store task into bag. taskBag.store(.init( - (state, action), + action, with: Task { // 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/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 From 3313e78e8135f17abfd232149ad434d87a4dbf43 Mon Sep 17 00:00:00 2001 From: Jeong Jineun Date: Sun, 24 Sep 2023 18:14:19 +0900 Subject: [PATCH 10/11] feat(): add annotations --- Sources/Reducer/Reduce.swift | 2 ++ Sources/Reducer/Reducer.swift | 7 +++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Sources/Reducer/Reduce.swift b/Sources/Reducer/Reduce.swift index 2d1a4e0..112feca 100644 --- a/Sources/Reducer/Reduce.swift +++ b/Sources/Reducer/Reduce.swift @@ -114,6 +114,7 @@ open class ProxyReduce: Reduce { } ) + // Set flag to false to indicate mutator pointer deallocated. _mutator.deallocate() isMutatorAllocated = false @@ -143,6 +144,7 @@ open class ProxyReduce: Reduce { deinit { if isMutatorAllocated { + // Deallocate pointer when mutator allocated. _mutator.deallocate() } } diff --git a/Sources/Reducer/Reducer.swift b/Sources/Reducer/Reducer.swift index 7eeac0e..39db564 100644 --- a/Sources/Reducer/Reducer.swift +++ b/Sources/Reducer/Reducer.swift @@ -8,7 +8,6 @@ import Foundation import Combine -@MainActor final class TaskBag { struct TaskItem: Hashable { // MARK: - Property @@ -81,9 +80,9 @@ open class Reducer: ObservableObject, Mutable { self.state = reduce.initialState self.reduce = reduce - // Start reduce with mutator. reduce.mutator = Mutator(self, initialState: reduce.initialState) Task { + // Start reduce with mutator. try? await reduce.start() } } @@ -95,7 +94,6 @@ open class Reducer: ObservableObject, Mutable { // MARK: - Lifecycle open func mutate(_ mutation: Mutation) { // Reduce state from mutation. - // Run task on main actor context. state = reduce(state: state, mutation: mutation) } @@ -103,6 +101,7 @@ open class Reducer: ObservableObject, Mutable { open func action(_ action: Action) { let reduce = reduce + // Traversing the task and deciding to cancel it. taskBag.forEach { task in guard reduce.shouldCancel(task.item, action) else { return } task.cancel() @@ -111,7 +110,7 @@ open class Reducer: ObservableObject, Mutable { // Store task into bag. taskBag.store(.init( action, - with: Task { + with: Task { @MainActor in // Mutate state from action. try? await reduce.mutate(action: action) } From 44fb74d5f809022c2af1ea95b23ddfa571097130 Mon Sep 17 00:00:00 2001 From: Jeong Jineun Date: Sun, 24 Sep 2023 18:14:47 +0900 Subject: [PATCH 11/11] feat(): update `README` to apply interface changes --- README.md | 56 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 6091e7d..ca09100 100644 --- a/README.md +++ b/README.md @@ -32,10 +32,15 @@ dependencies: [ > 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 @Reduce +@MainActor final class CounterReduce { // User interaction input. enum Action { @@ -44,7 +49,7 @@ final class CounterReduce { // Unit of state mutation. enum Mutation { - case addOne + case setCount(Int) } // Reducer state. @@ -58,10 +63,10 @@ final class CounterReduce { 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 { 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. @@ -143,6 +148,7 @@ If you want to make canceling a task meaningful, you'll need to [create a cancel ```swift @Reduce +@MainActor final class SignUpReduce { enum Action { case updateEmail(String) @@ -165,7 +171,7 @@ final class SignUpReduce { initialState = State(canSignUp: false) } - func mutate(state: State, action: Action) async throws { + func mutate(action: Action) async throws { switch action { case let .updateEmail(email): let result = try await validator.validate(email) @@ -180,8 +186,8 @@ final class SignUpReduce { ... - 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 @@ -195,10 +201,11 @@ final class SignUpReduce { ### 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 @Reduce +@MainActor final class ListReduce { enum Action { ... } @@ -213,18 +220,20 @@ final class ListReduce { } 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 + .sink { [weak self] data in // Write any mutates here. - mutator(.setList($0.object)) + 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) } } ``` @@ -260,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 ... } ))) ```