Skip to content

Commit

Permalink
Merge pull request #10 from wlsdms0122/develop
Browse files Browse the repository at this point in the history
[RELEASE] Reducer/2.0.0
  • Loading branch information
wlsdms0122 authored Sep 24, 2023
2 parents f66940c + caa518d commit 278cf78
Show file tree
Hide file tree
Showing 17 changed files with 368 additions and 167 deletions.
47 changes: 42 additions & 5 deletions .swiftpm/xcode/xcshareddata/xcschemes/Reducer.xcscheme
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1420"
version = "1.3">
LastUpgradeVersion = "1500"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
Expand All @@ -20,18 +20,55 @@
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "MacroTests"
BuildableName = "MacroTests"
BlueprintName = "MacroTests"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "ReducerTests"
BuildableName = "ReducerTests"
BlueprintName = "ReducerTests"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES">
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "MacroTests"
BuildableName = "MacroTests"
BlueprintName = "MacroTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "ReducerTests"
Expand Down
14 changes: 14 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"pins" : [
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-syntax.git",
"state" : {
"revision" : "e42bece52df2496d60b1b2a762fee9ffde7fc205",
"version" : "509.0.0-swift-DEVELOPMENT-SNAPSHOT-2023-08-07-a"
}
}
],
"version" : 2
}
21 changes: 18 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// swift-tools-version: 5.7
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription
import CompilerPluginSupport

let package = Package(
name: "Reducer",
Expand All @@ -19,13 +20,20 @@ let package = Package(
)
],
dependencies: [

.package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0")
],
targets: [
.macro(
name: "ReducerMacro",
dependencies: [
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax")
]
),
.target(
name: "Reducer",
dependencies: [

"ReducerMacro"
]
),
.testTarget(
Expand All @@ -34,5 +42,12 @@ let package = Package(
"Reducer"
]
),
.testTarget(
name: "MacroTests",
dependencies: [
"ReducerMacro",
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
]
)
]
)
103 changes: 62 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,60 +24,65 @@
## 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")
]
```

# Getting Started
> 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
}

// Unit of state mutation.
enum Mutation {
case addOne
case setCount(Int)
}

// Reducer state.
struct State {
var count: Int
}

var mutator: (any Mutator<Mutation, State>)?
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))
}
}

func reduce(state: State, mutation: Mutation) -> State {
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.

Expand Down Expand Up @@ -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 {
Expand All @@ -149,27 +163,36 @@ final class SignUpReduce: Reduce {
var canSignUp: Bool
}

var mutator: (any Mutator<Mutation, State>)?
var initialState: State
let initialState: State

private let validator = EmailValidator()

init() {
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
}
}
}
Expand All @@ -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 {
Expand All @@ -194,20 +219,21 @@ final class ListReduce: Reduce {
...
}

var mutator: (any Mutator<Mutation, State>)?
var initialState: State
let initialState: State
private var cancellableBag = Set<AnyCancellable>()

init() { ... }

func start(with mutator: any Mutator<Mutation, State>) 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)
}
}
```
Expand Down Expand Up @@ -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 ... }
)))
```

Expand Down
10 changes: 10 additions & 0 deletions Sources/Reducer/Macro/ReduceMacro.swift
Original file line number Diff line number Diff line change
@@ -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")
Loading

0 comments on commit 278cf78

Please sign in to comment.