diff --git a/README.md b/README.md index ca1d814..53188bf 100644 --- a/README.md +++ b/README.md @@ -21,10 +21,11 @@

-**OneWay** is a simple, lightweight library for state management using a unidirectional data flow, fully compatiable with Swift 6 and built on [Swift Concurrency](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/). Its structure makes it easier to maintain thread safety at all times. +**OneWay** is a simple, lightweight library for state management that uses a unidirectional data flow. It is fully compatible with Swift 6 and is built on [Swift Concurrency](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/). Its design ensures thread safety at all times. -It integrates effortlessly across platforms and frameworks, with zero third-party dependencies, allowing you to use it in its purest form. **OneWay** can be used anywhere, not just in the presentation layer, to simplify the complex business logic. If you're looking to implement unidirectional logic, **OneWay** is a straightforward and practical solution. +It integrates effortlessly across all Apple platforms and frameworks, with zero third-party dependencies, allowing you to use it in its purest form. **OneWay** can be used anywhere, not just in the presentation layer, to simplify complex business logic. If you are looking to implement unidirectional logic, **OneWay** is a straightforward and practical solution. +- [Features](#features) - [Data Flow](#data-flow) - [Usage](#usage) - [Documentation](#documentation) @@ -33,13 +34,22 @@ It integrates effortlessly across platforms and frameworks, with zero third-part - [Installation](#installation) - [References](#references) +## Features + +- 🕊️ **Lightweight and Simple**: A straightforward implementation of unidirectional data flow. +- 🔐 **Thread-Safe**: Built on Swift Concurrency to ensure thread safety. +- 🗂️ **Decoupled**: Aims for a clean separation between the view and business logic. +- 🥗 **Flexible**: Can be used in any part of your application, not just the presentation layer. +- 🧪 **Testable**: Provides a testing module to facilitate writing unit tests. +- ✨ **No Dependencies**: Has zero third-party dependencies. + ## Data Flow -When using the `Store`, the data flow is as follows. +When using a `Store`, the data flows in a single direction. flow_description_1 -When working on UI, it is better to use `ViewStore` to ensure main thread operation. +When working with UI, it is better to use a `ViewStore` to ensure all operations are performed on the main thread. flow_description_1 @@ -47,7 +57,7 @@ When working on UI, it is better to use `ViewStore` to ensure main thread operat ### Implementing a Reducer -After adopting the `Reducer` protocol, define the `Action` and `State`, and then implement the logic for each `Action` within the `reduce(state:action:)` function. +First, conform to the `Reducer` protocol, define your `Action` and `State`, and then implement the logic for each `Action` in the `reduce(state:action:)` method. ```swift struct CountingReducer: Reducer { @@ -90,7 +100,7 @@ struct CountingReducer: Reducer { ### Sending Actions -Sending an action to a **Store** causes changes in the `state` via `Reducer`. +Sending an action to a **Store** causes changes to the `state` through the `Reducer`. ```swift let store = Store( @@ -105,7 +115,7 @@ await store.send(.twice) print(await store.state.number) // 2 ``` -The usage is the same for `ViewStore`. However, when working within `MainActor`, such as in `UIViewController` or `View`'s body, `await` can be omitted. +The usage is the same for a `ViewStore`. However, when working within a `MainActor`, such as in a `UIViewController` or a `View`'s body, you can omit `await`. ```swift let store = ViewStore( @@ -120,9 +130,9 @@ store.send(.twice) print(store.state.number) // 2 ``` -### Observing States +### Observing State -When the state changes, you can receive a new state. It guarantees that the same state does not come down consecutively. +When the state changes, you will receive the new state. It is guaranteed that the same state will not be emitted consecutively. ```swift struct State: Sendable, Equatable { @@ -137,7 +147,7 @@ for await state in store.states { // Prints "10", "20" ``` -Of course, you can observe specific properties only. +Of course, you can also observe specific properties. ```swift // number <- 10, 10, 20 ,20 @@ -148,7 +158,7 @@ for await number in store.states.number { // Prints "10", "20" ``` -If you want to continue receiving the value even when the same value is assigned to the `State`, you can use `@Triggered`. For explanations of other useful property wrappers(e.g. [@CopyOnWrite](https://swiftpackageindex.com/devyeom/oneway/main/documentation/oneway/copyonwrite), [@Ignored](https://swiftpackageindex.com/devyeom/oneway/main/documentation/oneway/ignored)), refer to [here](https://swiftpackageindex.com/devyeom/oneway/main/documentation/oneway/triggered). +If you want to continue receiving values even when the same value is assigned to the `State`, you can use the `@Triggered` property wrapper. For explanations of other useful property wrappers, such as [@CopyOnWrite](https://swiftpackageindex.com/devyeom/oneway/main/documentation/oneway/copyonwrite) and [@Ignored](https://swiftpackageindex.com/devyeom/oneway/main/documentation/oneway/ignored), please refer to the [documentation](https://swiftpackageindex.com/devyeom/oneway/main/documentation/oneway/triggered). ```swift struct State: Sendable, Equatable { @@ -163,7 +173,7 @@ for await state in store.states { // Prints "10", "10", "20", "20" ``` -When there are multiple properties of the state, it is possible for the state to change due to other properties that are not subscribed to. In such cases, if you are using [AsyncAlgorithms](https://github.com/apple/swift-async-algorithms), you can remove duplicates as follows. +When there are multiple properties in the state, it is possible for the state to change due to other properties that you are not subscribed to. In such cases, if you are using [AsyncAlgorithms](https://github.com/apple/swift-async-algorithms), you can remove duplicates as follows. ```swift struct State: Sendable, Equatable { @@ -185,9 +195,9 @@ for await number in store.states.number.removeDuplicates() { // Prints "10" ``` -### Integration with SwiftUI +### Integrating with SwiftUI -It can be seamlessly integrated with [SwiftUI](https://developer.apple.com/documentation/swiftui). +**OneWay** can be seamlessly integrated with [SwiftUI](https://developer.apple.com/documentation/swiftui). ```swift struct CounterView: View { @@ -214,7 +224,7 @@ struct CounterView: View { } ``` -There is also a helper function that makes it easy to create [Binding](https://developer.apple.com/documentation/swiftui/binding). +There is also a helper method that makes it easy to create a [Binding](https://developer.apple.com/documentation/swiftui/binding). ```swift struct CounterView: View { @@ -242,7 +252,7 @@ For more details, please refer to the [examples](#examples). ### Cancelling Effects -You can make an effect capable of being canceled by using `cancellable()`. And you can use `cancel()` to cancel a cancellable effect. +You can make an effect cancellable by using the `cancellable()` method. You can then use `cancel()` to cancel the effect. ```swift func reduce(state: inout State, action: Action) -> AnyEffect { @@ -262,7 +272,7 @@ func reduce(state: inout State, action: Action) -> AnyEffect { } ``` -You can assign anything that conforms [Hashable](https://developer.apple.com/documentation/swift/hashable) as an identifier for the effect, not just a string. +You can use anything that conforms to the [Hashable](https://developer.apple.com/documentation/swift/hashable) protocol as an identifier for an effect, not just a string. ```swift enum EffectID { @@ -288,11 +298,11 @@ func reduce(state: inout State, action: Action) -> AnyEffect { ### Various Effects -**OneWay** supports various effects such as `just`, `concat`, `merge`, `single`, `sequence`, and more. For more details, please refer to the [documentation](https://swiftpackageindex.com/devyeom/oneway/main/documentation/oneway/effects). +**OneWay** supports various effects, such as `just`, `concat`, `merge`, `single`, `sequence`, and more. For more details, please refer to the [documentation](https://swiftpackageindex.com/devyeom/oneway/main/documentation/oneway/effects). -### External States +### External State -You can easily receive to external states by implementing `bind()`. If there are changes in publishers or streams that necessitate rebinding, you can call `reset()` of `Store`. +You can easily subscribe to external state changes by implementing the `bind()` method. If there are changes in publishers or streams that require re-binding, you can call the `reset()` method of the `Store`. ```swift let textPublisher = PassthroughSubject() @@ -320,9 +330,9 @@ struct CountingReducer: Reducer { ### Testing -**OneWay** provides the `expect` function to help you write concise and clear tests. This function works asynchronously, allowing you to verify whether the state updates as expected. +**OneWay** provides an `expect` function to help you write concise and clear tests. This function works asynchronously, allowing you to verify that the state updates as expected. -Before using the `expect` function, make sure to import the **OneWayTesting** module. +Before using the `expect` function, be sure to import the **OneWayTesting** module. ```swift import OneWayTesting @@ -344,7 +354,7 @@ func incrementTwice() async { #### When using XCTest -The `expect` function is used in the same way within the `XCTest` environment. +The `expect` function is used in the same way in an `XCTest` environment. ```swift func test_incrementTwice() async { @@ -359,7 +369,7 @@ For more details, please refer to the [Testing](https://swiftpackageindex.com/De ## Documentation -To learn how to use **OneWay** in more detail, go through the [documentation](https://swiftpackageindex.com/DevYeom/OneWay/main/documentation/OneWay). +To learn how to use **OneWay** in more detail, please refer to the [documentation](https://swiftpackageindex.com/DevYeom/OneWay/main/documentation/OneWay). ## Examples @@ -378,13 +388,13 @@ To learn how to use **OneWay** in more detail, go through the [documentation](ht ## Installation -**OneWay** is only supported by Swift Package Manager. +**OneWay** is exclusively supported by the Swift Package Manager. -To integrate **OneWay** into your Xcode project using Swift Package Manager, add it to the dependencies value of your `Package.swift`: +To integrate **OneWay** into your Xcode project using the Swift Package Manager, add it to the dependencies in your `Package.swift` file. ```swift dependencies: [ - .package(url: "https://github.com/DevYeom/OneWay", from: "2.0.0"), + .package(url: "https://github.com/DevYeom/OneWay", from: "3.0.0"), ] ``` diff --git a/Sources/OneWay/AnyEffect.swift b/Sources/OneWay/AnyEffect.swift index d639b98..d221526 100644 --- a/Sources/OneWay/AnyEffect.swift +++ b/Sources/OneWay/AnyEffect.swift @@ -10,7 +10,7 @@ public struct AnyEffect: Effect where Element: Sendable { /// A convenience type alias for representing a hashable identifier. public typealias EffectID = Hashable & Sendable - /// Enumeration of methods representing additional functionality for AnyEffect. + /// An enumeration of methods that represent additional functionality for an `AnyEffect`. public enum Method: Sendable { case register(any EffectID, cancelInFlight: Bool) case cancel(any EffectID) @@ -18,7 +18,7 @@ public struct AnyEffect: Effect where Element: Sendable { case none } - /// A method of the AnyEffect + /// The method of the `AnyEffect`. public var method: Method = .none public var values: AsyncStream { base.values } @@ -33,10 +33,10 @@ public struct AnyEffect: Effect where Element: Sendable { self.base = base } - /// Assigning an identifier to make it possible to cancel the effect. + /// Assigns an identifier to make it possible to cancel the effect. /// - /// - Parameter id: The effect's identifier. - /// - Returns: A new effect that can be canceled by an identifier. + /// - Parameter id: The identifier for the effect. + /// - Returns: A new effect that can be canceled by its identifier. public consuming func cancellable( _ id: some EffectID ) -> AnyEffect { @@ -45,9 +45,9 @@ public struct AnyEffect: Effect where Element: Sendable { return copy } - /// Sends elements only after a specified time interval elapses between events. + /// Sends elements only after a specified time interval has elapsed between events. /// - /// First, create a Hashable ID that will be used to identify the debounce effect: + /// First, create a `Hashable` ID that will be used to identify the debounce effect. /// /// ```swift /// enum DebounceID { @@ -55,65 +55,7 @@ public struct AnyEffect: Effect where Element: Sendable { /// } /// ``` /// - /// Then, apply the `debounce` modifier using the defined ID: - /// - /// ```swift - /// func reduce(state: inout State, action: Action) -> AnyEffect { - /// switch action { - /// // ... - /// case let .search(text): - /// return .single { - /// let result = await api.request(text) - /// return .setResult(result) - /// } - /// .debounce(id: DebounceID.searchText, for: 0.5) - /// // ... - /// } - /// } - /// ``` - /// - /// - Parameters: - /// - id: The effect's identifier. - /// - seconds: The duration for which the effect should wait before sending an element. - /// - Returns: A new effect that sends elements only after a specified time elapses. - @available(*, deprecated, renamed: "debounce(id:for:clock:)") - public consuming func debounce( - id: some EffectID, - for seconds: Double - ) -> Self { - let base = base - var copy = self - copy.method = .register(id, cancelInFlight: true) - copy.base = Effects.Sequence( - operation: { send in - guard !Task.isCancelled else { return } - let NSEC_PER_SEC: Double = 1_000_000_000 - let dueTime = NSEC_PER_SEC * seconds - do { - try await Task.sleep(nanoseconds: UInt64(dueTime)) - } catch { - return - } - for await value in base.values { - guard !Task.isCancelled else { return } - send(value) - } - } - ) - return copy - } - - /// Sends elements only after a specified time interval elapses between events. - /// - /// First, create a Hashable ID that will be used to identify the debounce effect: - /// - /// ```swift - /// enum DebounceID { - /// case searchText - /// } - /// ``` - /// - /// Then, apply the `debounce` modifier using the defined ID: + /// Then, apply the `debounce` modifier using the defined ID. /// /// ```swift /// func reduce(state: inout State, action: Action) -> AnyEffect { @@ -131,10 +73,10 @@ public struct AnyEffect: Effect where Element: Sendable { /// ``` /// /// - Parameters: - /// - id: The effect's identifier. + /// - id: The identifier for the effect. /// - dueTime: The duration for which the effect should wait before sending an element. /// - clock: The clock used for measuring time intervals. - /// - Returns: A new effect that sends elements only after a specified time elapses. + /// - Returns: A new effect that sends elements only after a specified time has elapsed. public consuming func debounce( id: some EffectID, for dueTime: C.Instant.Duration, @@ -163,7 +105,7 @@ public struct AnyEffect: Effect where Element: Sendable { /// Creates an effect that emits elements from this effect, but only if a certain amount of time /// has passed between emissions. /// - /// First, create a `Hashable` ID that will be used to identify the throttle effect: + /// First, create a `Hashable` ID that will be used to identify the throttle effect. /// /// ```swift /// enum ThrottleID { @@ -171,7 +113,7 @@ public struct AnyEffect: Effect where Element: Sendable { /// } /// ``` /// - /// Then, apply the `throttle` modifier using the defined ID: + /// Then, apply the `throttle` modifier using the defined ID. /// /// ```swift /// func reduce(state: inout State, action: Action) -> AnyEffect { @@ -179,19 +121,19 @@ public struct AnyEffect: Effect where Element: Sendable { /// // ... /// case .perform: /// return .just(.increment) - /// .throttle(id: ThrottleID.button, for: .seconds(1)) + /// .throttle(id: ThrottleID.button, for: .seconds(1), latest: true) /// // ... /// } /// } /// ``` /// /// - Parameters: - /// - id: The effect’s identifier. + /// - id: The identifier for the effect. /// - interval: The duration that must elapse before another element can be emitted. - /// - latest: A Boolean value indicating whether to emit the most recent element. + /// - latest: A boolean value that indicates whether to emit the most recent element. /// If `false`, the effect emits the first element and ignores subsequent ones during the /// interval. If `true`, it emits the first element and then the most recent element once - /// the interval has passed. Defaults to `false`. + /// the interval has passed. The default is `false`. /// - Returns: A new effect that emits elements according to the throttle behavior. public consuming func throttle( id: some EffectID, @@ -205,8 +147,10 @@ public struct AnyEffect: Effect where Element: Sendable { } extension AnyEffect { - /// An effect that does nothing and finishes immediately. It is useful for situations where you - /// must return a effect, but you don't need to do anything. + /// An effect that does nothing and finishes immediately. + /// + /// This is useful for situations where you must return an effect, but you do not need to + /// perform any operations. @inlinable public static var none: AnyEffect { Effects.Empty().eraseToAnyEffect() @@ -223,7 +167,7 @@ extension AnyEffect { Effects.Just(element).eraseToAnyEffect() } - /// An effect that allows canceling by using an identifier. + /// An effect that allows for cancellation by using an identifier. /// /// - Parameter id: The identifier of the effect to be canceled. /// - Returns: A new effect. @@ -241,7 +185,7 @@ extension AnyEffect { /// - Parameters: /// - priority: The priority of the task. /// Pass `nil` to use the priority from `Task.currentPriority`. - /// - operation: The operation to perform. + /// - operation: The operation to be performed. /// - Returns: A new effect. @inlinable public static func single( @@ -254,13 +198,14 @@ extension AnyEffect { ).eraseToAnyEffect() } - /// An effect that can supply multiple values asynchronously in the future. It can be used for - /// observing an asynchronous sequence. + /// An effect that can supply multiple values asynchronously in the future. + /// + /// This can be used for observing an asynchronous sequence. /// /// - Parameters: /// - priority: The priority of the task. /// Pass `nil` to use the priority from `Task.currentPriority`. - /// - operation: The operation to perform. + /// - operation: The operation to be performed. /// - Returns: A new effect. @inlinable public static func sequence( @@ -273,13 +218,13 @@ extension AnyEffect { ).eraseToAnyEffect() } - /// An effect that concatenates a list of effects together into a single effect, which runs the + /// An effect that concatenates a list of effects into a single effect, which runs the /// effects one after the other. /// /// - Parameters: /// - priority: The priority of the task. /// Pass `nil` to use the priority from `Task.currentPriority`. - /// - effects: Variadic effects. + /// - effects: A variadic list of effects. /// - Returns: A new effect. @inlinable public static func concat( @@ -292,13 +237,13 @@ extension AnyEffect { ).eraseToAnyEffect() } - /// An effect that concatenates a list of effects together into a single effect, which runs the + /// An effect that concatenates a list of effects into a single effect, which runs the /// effects one after the other. /// /// - Parameters: /// - priority: The priority of the task. /// Pass `nil` to use the priority from `Task.currentPriority`. - /// - build: A builder that makes effects. + /// - build: A builder that creates effects. /// - Returns: A new effect. @inlinable public static func concat( @@ -311,13 +256,13 @@ extension AnyEffect { ).eraseToAnyEffect() } - /// An effect that merges a list of effects together into a single effect, which runs the - /// effects at the same time. + /// An effect that merges a list of effects into a single effect, which runs the effects + /// at the same time. /// /// - Parameters: /// - priority: The priority of the task. /// Pass `nil` to use the priority from `Task.currentPriority`. - /// - effects: Variadic effects. + /// - effects: A variadic list of effects. /// - Returns: A new effect. @inlinable public static func merge( @@ -330,13 +275,13 @@ extension AnyEffect { ).eraseToAnyEffect() } - /// An effect that merges a list of effects together into a single effect, which runs the - /// effects at the same time. + /// An effect that merges a list of effects into a single effect, which runs the effects + /// at the same time. /// /// - Parameters: /// - priority: The priority of the task. /// Pass `nil` to use the priority from `Task.currentPriority`. - /// - build: A builder that makes effects. + /// - build: A builder that creates effects. /// - Returns: A new effect. @inlinable public static func merge( @@ -352,12 +297,12 @@ extension AnyEffect { /// An effect that creates an asynchronous stream. /// /// - Parameters: - /// - bufferingPolicy: A `Continuation.BufferingPolicy` value to set the stream's buffering - /// behavior. By default, the stream buffers an unlimited number of elements. You can also set - /// the policy to buffer a specified number of oldest or newest elements. - /// - build: A custom closure that yields values to the `AsyncStream`. This closure receives - /// an `AsyncStream.Continuation` instance that it uses to provide elements to the stream and - /// terminate the stream when finished. + /// - bufferingPolicy: A `Continuation.BufferingPolicy` value to set the stream's + /// buffering behavior. By default, the stream buffers an unlimited number of elements. You + /// can also set the policy to buffer a specified number of the oldest or newest elements. + /// - build: A custom closure that yields values to the `AsyncStream`. This closure + /// receives an `AsyncStream.Continuation` instance that it uses to provide elements to the + /// stream and to terminate the stream when it is finished. /// - Returns: A new effect. @inlinable public static func create( diff --git a/Sources/OneWay/AsyncSequences/AsyncViewStateSequence.swift b/Sources/OneWay/AsyncSequences/AsyncViewStateSequence.swift index 1255929..715e502 100644 --- a/Sources/OneWay/AsyncSequences/AsyncViewStateSequence.swift +++ b/Sources/OneWay/AsyncSequences/AsyncViewStateSequence.swift @@ -5,9 +5,9 @@ // Copyright (c) 2022-2025 SeungYeop Yeom ( https://github.com/DevYeom ). // -/// An asynchronous sequence of the ``ViewStore``'s state. +/// An asynchronous sequence of a ``ViewStore``'s state. /// -/// This stream supports dynamic member lookup so that you can pluck out a specific field in the +/// This stream supports dynamic member lookup, allowing you to extract a specific field from the /// state. @MainActor @dynamicMemberLookup @@ -15,7 +15,7 @@ public final class AsyncViewStateSequence: AsyncSequence where State: Sendable & Equatable { public typealias Element = State - /// The iterator for an `AsyncViewStateSequence` instance. + /// The iterator for an `AsyncViewStateSequence`. public struct Iterator: AsyncIteratorProtocol { public typealias Element = State @@ -59,10 +59,10 @@ where State: Sendable & Equatable { } } - /// Returns the resulting stream with partial state corresponding to the given key path. + /// Returns the resulting stream with a partial state corresponding to the given key path. /// - /// - Parameter dynamicMember: a key path for the original state. - /// - Returns: A new stream that has a part of the original state. + /// - Parameter dynamicMember: A key path for the original state. + /// - Returns: A new stream that contains a part of the original state. #if swift(>=6.0) public subscript( dynamicMember keyPath: KeyPath & Sendable diff --git a/Sources/OneWay/Effect.swift b/Sources/OneWay/Effect.swift index e77e6f1..b28fd89 100644 --- a/Sources/OneWay/Effect.swift +++ b/Sources/OneWay/Effect.swift @@ -5,21 +5,23 @@ // Copyright (c) 2022-2025 SeungYeop Yeom ( https://github.com/DevYeom ). // -/// A protocol encapsulating a unit of work that can be executed in the external environment and can -/// provide data to the ``Store``. +/// A protocol that encapsulates a unit of work that can be executed in an external environment and +/// can provide data to a ``Store``. /// -/// This is the perfect place to handle side effects, including network requests, saving/loading -/// from disk, creating timers, interacting with dependencies, and more. Effects are returned from -/// reducers, allowing the ``Store`` to execute them once the reducer has finished its operation. +/// This is the perfect place to handle side effects, such as network requests, saving or loading +/// from disk, creating timers, and interacting with dependencies. Effects are returned from +/// reducers, allowing the ``Store`` to execute them after the reducer has finished its operation. public protocol Effect: Sendable { associatedtype Element: Sendable - /// The elements produced by the effect, as an asynchronous sequence. + /// The elements produced by the effect, delivered as an asynchronous sequence. var values: AsyncStream { get } } extension Effect { /// Wraps this effect with a type eraser. + /// + /// - Returns: An `AnyEffect` wrapping this effect. public func eraseToAnyEffect() -> AnyEffect { AnyEffect(self) } @@ -27,10 +29,12 @@ extension Effect { /// A namespace for types that serve as effects. public enum Effects { - /// An effect that does nothing and finishes immediately. It is useful for situations where you - /// must return a effect, but you don't need to do anything. + /// An effect that does nothing and finishes immediately. + /// + /// This is useful for situations where you must return an effect, but you do not need to + /// perform any operations. public struct Empty: Effect where Element: Sendable { - /// Initializes a `Empty` effect. + /// Initializes an `Empty` effect. public init() { } public var values: AsyncStream { @@ -46,7 +50,7 @@ public enum Effects { /// Initializes a `Just` effect. /// - /// - Parameter element: An element to emit immediately. + /// - Parameter element: An element to be emitted immediately. public init(_ element: Element) { self.element = element } @@ -69,7 +73,7 @@ public enum Effects { /// - Parameters: /// - priority: The priority of the task. /// Pass `nil` to use the priority from `Task.currentPriority`. - /// - operation: The operation to perform. + /// - operation: The operation to be performed. public init( priority: TaskPriority? = nil, operation: @Sendable @escaping () async -> Element @@ -92,8 +96,9 @@ public enum Effects { } } - /// An effect that can supply multiple values asynchronously in the future. It can be used for - /// observing an asynchronous sequence. + /// An effect that can supply multiple values asynchronously in the future. + /// + /// This can be used for observing an asynchronous sequence. public struct Sequence: Effect where Element: Sendable { private let priority: TaskPriority? private let operation: @Sendable (@escaping (Element) -> Void) async -> Void @@ -103,7 +108,7 @@ public enum Effects { /// - Parameters: /// - priority: The priority of the task. /// Pass `nil` to use the priority from `Task.currentPriority`. - /// - operation: The operation to perform. + /// - operation: The operation to be performed. public init( priority: TaskPriority? = nil, operation: @Sendable @escaping (@escaping (Element) -> Void) async -> Void @@ -125,7 +130,7 @@ public enum Effects { } } - /// An effect that concatenates a list of effects together into a single effect, which runs the + /// An effect that concatenates a list of effects into a single effect, which runs the /// effects one after the other. public struct Concat: Effect where Element: Sendable { private let priority: TaskPriority? @@ -163,8 +168,8 @@ public enum Effects { } } - /// An effect that merges a list of effects together into a single effect, which runs the - /// effects at the same time. + /// An effect that merges a list of effects into a single effect, which runs the effects + /// at the same time. public struct Merge: Effect where Element: Sendable { private let priority: TaskPriority? private let effects: [AnyEffect] @@ -227,11 +232,12 @@ public enum Effects { /// /// - Parameters: /// - bufferingPolicy: A `Continuation.BufferingPolicy` value to set the stream's - /// buffering behavior. By default, the stream buffers an unlimited number of elements. - /// You can also set the policy to buffer a specified number of oldest or newest elements. + /// buffering behavior. By default, the stream buffers an unlimited number of elements. + /// You can also set the policy to buffer a specified number of the oldest or newest + /// elements. /// - build: A custom closure that yields values to the `AsyncStream`. This closure - /// receives an `AsyncStream.Continuation` instance that it uses to provide elements to - /// the stream and terminate the stream when finished. + /// receives an `AsyncStream.Continuation` instance that it uses to provide elements to + /// the stream and to terminate the stream when it is finished. public init( bufferingPolicy: AsyncStream.Continuation.BufferingPolicy = .unbounded, build: @escaping (AsyncStream.Continuation) -> Void diff --git a/Sources/OneWay/EffectsBuilder.swift b/Sources/OneWay/EffectsBuilder.swift index e2ed072..d5a800e 100644 --- a/Sources/OneWay/EffectsBuilder.swift +++ b/Sources/OneWay/EffectsBuilder.swift @@ -7,6 +7,7 @@ import Foundation +/// A result builder for composing effects. @resultBuilder public enum EffectsBuilder { public static func buildArray(_ components: [[AnyEffect]]) -> [AnyEffect] { diff --git a/Sources/OneWay/OneWay.docc/Articles/Debugging.md b/Sources/OneWay/OneWay.docc/Articles/Debugging.md new file mode 100644 index 0000000..05fe5e2 --- /dev/null +++ b/Sources/OneWay/OneWay.docc/Articles/Debugging.md @@ -0,0 +1,117 @@ +# Debugging + +Using the `debug()` function to log actions and state changes. + +## Overview + +**OneWay** provides a `debug()` function on both `Store` and `ViewStore` to help you understand how data flows through your application. When enabled, it logs actions and state changes to the console, making it easier to trace the sequence of events and diagnose issues. + +The `debug()` function takes a `LoggingOptions` parameter, which can be one of the following: + +- `.action`: Logs only the actions that are sent. +- `.state`: Logs only the state changes that occur. +- `.all`: Logs both actions and state changes. +- `.none`: Disables all logging. + +### Using with Store + +You can configure logging for a `Store` instance either during initialization or by calling `debug()` later. This is useful when you are working with business logic outside of the UI layer. + +**Configuring logging during initialization:** + +```swift +let store = Store( + reducer: CountingReducer(), + state: CountingReducer.State(number: 0), + loggingOptions: .all // Enable logging for all actions and state changes from the start +) + +await store.send(.increment) +await store.send(.decrement) +``` + +**Configuring logging after initialization:** + +```swift +let store = Store( + reducer: CountingReducer(), + state: CountingReducer.State(number: 0) +) + +// Enable logging for all actions and state changes +await store.debug(.all) + +await store.send(.increment) +await store.send(.decrement) + +// Disable logging +await store.debug(.none) +``` + +When running the code above, you will see logs in the console similar to this: + +``` +[2025-11-15T12:34:56.789Z] Action: increment +[2025-11-15T12:34:56.790Z] State changed: +- State(number: 0) ++ State(number: 1) +[2025-11-15T12:34:56.791Z] Action: decrement +[2025-11-15T12:34:56.792Z] State changed: +- State(number: 1) ++ State(number: 0) +``` + +### Using with ViewStore + +When working with SwiftUI, you can use the `debug()` modifier on a `ViewStore` to enable logging. This is particularly helpful for debugging UI-related state changes. + +**Configuring logging during initialization:** + +```swift +struct CounterView: View { + @StateObject private var store = ViewStore( + reducer: CountingReducer(), + state: CountingReducer.State(number: 0) + ) + .debug(.all) // Enable logging for this view's store + + var body: some View { + VStack { + Text("\(store.state.number)") + Button("Increment") { + store.send(.increment) + } + } + } +} +``` + +**Configuring logging after initialization:** + +```swift +struct AnotherCounterView: View { + @StateObject private var store = ViewStore( + reducer: CountingReducer(), + state: CountingReducer.State(number: 0) + ) + + var body: some View { + VStack { + Text("\(store.state.number)") + Button("Increment") { + store.send(.increment) + } + Button("Toggle Logging") { + // Dynamically enable or disable logging + if store.loggingOptions.contains(.all) { + store.debug(.none) + } else { + store.debug(.all) + } + } + } + } +} +``` + +With this setup, any interaction that triggers an action or a state change in `CounterView` will be logged to the console, providing a clear picture of what is happening in your UI. diff --git a/Sources/OneWay/OneWay.docc/Articles/Testing.md b/Sources/OneWay/OneWay.docc/Articles/Testing.md index f9fccc1..368b212 100644 --- a/Sources/OneWay/OneWay.docc/Articles/Testing.md +++ b/Sources/OneWay/OneWay.docc/Articles/Testing.md @@ -1,18 +1,18 @@ # Testing -Using OneWay for Unit Testing. +Using **OneWay** for unit testing. ## Overview -**OneWay** provides the `expect` function to help you write concise and clear tests. This function works asynchronously, allowing you to verify whether the state updates as expected. +**OneWay** provides an `expect` function to help you write concise and clear tests. This function works asynchronously, allowing you to verify that the state updates as expected. -Before using the `expect` function, make sure to import the **OneWayTesting** module. +Before using the `expect` function, be sure to import the **OneWayTesting** module. ```swift import OneWayTesting ``` -When testing a reducer, you need to use `Store` instead of `ViewStore` for the `expect` function to be available. +When testing a reducer, you should use a `Store` instead of a `ViewStore` to have access to the `expect` function. ```swift let sut = Store( @@ -23,9 +23,9 @@ await sut.send(.increment) await sut.expect(\.count, 1) ``` -The completion of `send`'s `await` literally means that `send` has finished. It does not mean that the state has fully changed. The state change always happpens asynchronously. Therefore, tests should be written using the `expect` function. +The completion of `await` on `send` only indicates that the action has been sent, not that the state has been fully updated. State changes always occur asynchronously. Therefore, tests should be written using the `expect` function to ensure that you are asserting against the final state. -#### When using Testing +### When using Testing You can use the `expect` function to easily check the state value. @@ -39,9 +39,9 @@ func incrementTwice() async { } ``` -#### When using XCTest +### When using XCTest -The `expect` function is used in the same way within the `XCTest` environment. +The `expect` function is used in the same way in an `XCTest` environment. ```swift func test_incrementTwice() async { @@ -52,11 +52,11 @@ func test_incrementTwice() async { } ``` -## Specifying timeout if needed +## Specifying a Timeout -Both functions include a `timeout` parameter, which specifies the maximum amount of time (in seconds) to wait for the state to finish processing before timing out. The default value is 2 seconds. +The `expect` function includes a `timeout` parameter, which specifies the maximum amount of time (in seconds) to wait for the state to finish processing before timing out. The default value is 2 seconds. -#### When using Testing +### When using Testing ```swift @Test @@ -68,7 +68,7 @@ func incrementTwice() async { } ``` -#### When using XCTest +### When using XCTest ```swift func test_incrementTwice() async { @@ -83,10 +83,10 @@ func test_incrementTwice() async { When a test fails, the output provides detailed information about the failure, making it easier to diagnose the issue. Below are example screenshots showing how a failure appears. -#### When using Testing +### When using Testing -![failure with expect](expect-testing-failure.png) +![A screenshot of a test failure in Xcode, showing the expected and actual values.](expect-testing-failure.png) -#### When using XCTest +### When using XCTest -![failure with xctExpect](expect-xctest-failure.png) +![A screenshot of a test failure in Xcode, showing a descriptive failure message from an XCTest assertion.](expect-xctest-failure.png) diff --git a/Sources/OneWay/OneWay.docc/Articles/ThrottlingAndDebouncing.md b/Sources/OneWay/OneWay.docc/Articles/ThrottlingAndDebouncing.md new file mode 100644 index 0000000..603d6c8 --- /dev/null +++ b/Sources/OneWay/OneWay.docc/Articles/ThrottlingAndDebouncing.md @@ -0,0 +1,62 @@ +# Throttling and Debouncing + +Controlling the timing of events using `throttle` and `debounce`. + +## Overview + +**OneWay** provides `throttle` and `debounce` to control how frequently an effect can emit values. These are useful for managing events that can occur in rapid succession, such as user input or notifications. + +### Throttling + +Throttling limits the emission of values to a specified time interval. For example, if you throttle an effect to once per second, it will emit the first value and then ignore all subsequent values for the next second. + +To use `throttle`, you first need to define a `Hashable` identifier for the effect. + +```swift +enum ThrottleID { + case button +} +``` + +Then, apply the `throttle` modifier to your effect. + +```swift +func reduce(state: inout State, action: Action) -> AnyEffect { + switch action { + case .perform: + return .just(.increment) + .throttle(id: ThrottleID.button, for: .seconds(1), latest: false) + // ... + } +} +``` + +### Debouncing + +Debouncing delays the emission of values until a specified time has passed without any new values being emitted. This is useful for handling user input, such as in a search field, where you only want to perform a search after the user has stopped typing. + +To use `debounce`, you also need a `Hashable` identifier. + +```swift +enum DebounceID { + case searchText +} +``` + +Then, apply the `debounce` modifier to your effect. + +```swift +func reduce(state: inout State, action: Action) -> AnyEffect { + switch action { + case let .search(text): + return .single { + let result = await api.request(text) + return .setResult(result) + } + .debounce(id: DebounceID.searchText, for: .milliseconds(500)) + // ... + } +} +``` + +By using `throttle` and `debounce`, you can effectively control the flow of events in your application, improving performance and user experience. diff --git a/Sources/OneWay/OneWay.docc/OneWay.md b/Sources/OneWay/OneWay.docc/OneWay.md index 39e463c..fcbe8df 100644 --- a/Sources/OneWay/OneWay.docc/OneWay.md +++ b/Sources/OneWay/OneWay.docc/OneWay.md @@ -1,10 +1,10 @@ # ``OneWay`` -A Swift library for state management with unidirectional data flow. +A Swift library for state management with a unidirectional data flow. ## Overview -**OneWay** is a simple, lightweight library for state management using a unidirectional data flow, fully compatiable with Swift 6 and built on [Swift Concurrency](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/). Its structure makes it easier to maintain thread safety at all times. +**OneWay** is a simple, lightweight library for state management that uses a unidirectional data flow. It is fully compatible with Swift 6 and is built on [Swift Concurrency](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/), which ensures thread safety at all times. ```swift // Define a reducer @@ -47,24 +47,27 @@ struct CountingReducer: Reducer { } ``` -Whether you're using UIKit or SwiftUI, you can seamlessly apply it everywhere and utilize it in all places where you want to simplify complex logic in a one-way direction, not just in the presentation layer. +Whether you are using UIKit or SwiftUI, you can seamlessly apply it anywhere you want to simplify complex logic with a unidirectional data flow, not just in the presentation layer. ## Requirements -| OneWay | Swift | Xcode | Platforms | -|--------|-------|-------|-----------------------------------------------| -| 2.0 | 5.9 | 15.0 | iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0 | -| 1.0 | 5.5 | 13.0 | iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0 | +| OneWay | Swift | Xcode | Platforms | +|--------|-------|-------|-------------------------------------------------------------| +| 3.0 | 6.0 | 16.0 | iOS 16.0, macOS 13, tvOS 16.0, visionOS 1.0, watchOS 9.0 | +| 2.0 | 5.9 | 15.0 | iOS 13.0, macOS 10.15, tvOS 13.0, visionOS 1.0, watchOS 6.0 | +| 1.0 | 5.5 | 13.0 | iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0 | ## License -OneWay is released under the MIT license. See LICENSE for details. +**OneWay** is released under the MIT license. See [LICENSE](LICENSE) for details. ## Topics ### Articles +- doc:Debugging - doc:Testing +- doc:ThrottlingAndDebouncing ### Essentials diff --git a/Sources/OneWay/PropertyWrappers/CopyOnWrite.swift b/Sources/OneWay/PropertyWrappers/CopyOnWrite.swift index ac41718..f7859aa 100644 --- a/Sources/OneWay/PropertyWrappers/CopyOnWrite.swift +++ b/Sources/OneWay/PropertyWrappers/CopyOnWrite.swift @@ -5,14 +5,14 @@ // Copyright (c) 2022-2025 SeungYeop Yeom ( https://github.com/DevYeom ). // -/// A property wrapper that facilitates the use of copy-on-write semantics to eliminate the cost of -/// copying large values. +/// A property wrapper that provides copy-on-write semantics to eliminate the cost of copying +/// large values. /// -/// When applied to a field, the corresponding value will always be heap-allocated because this -/// wrapper uses a class, and classes in Swift are always allocated on the heap. This is especially -/// useful for large value types, as it prevents stack overflow by ensuring the value is managed on -/// the heap instead of the stack. Additionally, the copy-on-write behavior helps avoid unnecessary -/// copying when the value is not modified, improving performance. +/// When applied to a field, the value will always be allocated on the heap because this wrapper +/// uses a class, and classes in Swift are always heap-allocated. This is especially useful for +/// large value types, as it prevents stack overflow by managing the value on the heap instead of +/// the stack. Additionally, the copy-on-write behavior helps avoid unnecessary copying when the +/// value is not modified, thereby improving performance. /// /// - SeeAlso: [Advice: Use copy-on-write semantics for large values](https://github.com/swiftlang/swift/blob/swift-6.0-RELEASE/docs/OptimizationTips.rst#advice-use-copy-on-write-semantics-for-large-values) @propertyWrapper diff --git a/Sources/OneWay/PropertyWrappers/Ignored.swift b/Sources/OneWay/PropertyWrappers/Ignored.swift index ee593af..35678f0 100644 --- a/Sources/OneWay/PropertyWrappers/Ignored.swift +++ b/Sources/OneWay/PropertyWrappers/Ignored.swift @@ -5,12 +5,13 @@ // Copyright (c) 2022-2025 SeungYeop Yeom ( https://github.com/DevYeom ). // -/// A property wrapper that acts like the same value, regardless of changes in its actual value. +/// A property wrapper that always evaluates as equal, regardless of the wrapped value. /// -/// When applied to a field and compared with an equals sign, it will always yield `true`. This -/// prevents the observers from recognizing the state as changed, even if the actual value has been -/// updated. It is particularly useful when you want to avoid triggering state changes, especially -/// in SwiftUI, where unnecessary re-renders of the `View` can be avoided. +/// When this property wrapper is applied to a field and compared for equality, it will always +/// return `true`. This prevents observers from detecting a state change, even if the underlying + +/// value has been updated. It is particularly useful for avoiding unnecessary UI updates in +/// SwiftUI when a property should not trigger a re-render. @propertyWrapper public struct Ignored { public var wrappedValue: Value diff --git a/Sources/OneWay/PropertyWrappers/Triggered.swift b/Sources/OneWay/PropertyWrappers/Triggered.swift index e88aa90..06e50b6 100644 --- a/Sources/OneWay/PropertyWrappers/Triggered.swift +++ b/Sources/OneWay/PropertyWrappers/Triggered.swift @@ -5,13 +5,12 @@ // Copyright (c) 2022-2025 SeungYeop Yeom ( https://github.com/DevYeom ). // -/// A property wrapper that makes a value behave like a different one just by assigning it, even if -/// the same value is assigned. +/// A property wrapper that treats a value as unequal, even if the same value is assigned. /// -/// When applied to a field, assigning the same value will lead to different values for comparison. -/// This ensures that observers will perceive the state as changed, even if the value itself remains -/// the same. It is particularly useful in SwiftUI, as it can trigger the `View` to re-render when -/// necessary, even if the value hasn't technically changed. +/// When this property wrapper is applied to a field, assigning the same value will result in a +/// different internal state. This ensures that observers will perceive the state as changed, even +/// if the value itself remains the same. It is particularly useful in SwiftUI, as it can trigger a +/// `View` to re-render when needed, even if the value has not technically changed. @propertyWrapper public struct Triggered where Value: Equatable { fileprivate struct Storage: Equatable { diff --git a/Sources/OneWay/Reducer.swift b/Sources/OneWay/Reducer.swift index 56316cd..974463a 100644 --- a/Sources/OneWay/Reducer.swift +++ b/Sources/OneWay/Reducer.swift @@ -5,19 +5,31 @@ // Copyright (c) 2022-2025 SeungYeop Yeom ( https://github.com/DevYeom ). // -/// A protocol defining a reduce fuction to transition the current state to the next state and a -/// bind function for observing global states. +/// A protocol that defines a reduce function to transition the current state to the next state and +/// a bind function for observing global states. public protocol Reducer: Sendable { associatedtype Action associatedtype State: Equatable + /// A function that observes and responds to external changes. + /// + /// You can use this function to subscribe to notifications or other data sources that are not + /// directly tied to the view. + /// + /// - Returns: An effect that should be executed in response to the observed changes. func bind() -> AnyEffect + + /// A function that transitions the current state to the next state in response to an action. + /// + /// - Parameters: + /// - state: The current state of the feature. + /// - action: An action that has been sent. + /// - Returns: An effect that can be executed by the `Store`. func reduce(state: inout State, action: Action) -> AnyEffect } extension Reducer where Action: Sendable { public func bind() -> AnyEffect { - // Default implementation - return .none + .none } } diff --git a/Sources/OneWay/Store.swift b/Sources/OneWay/Store.swift index e80306b..7aa54e3 100644 --- a/Sources/OneWay/Store.swift +++ b/Sources/OneWay/Store.swift @@ -14,23 +14,24 @@ import OSLog /// `Store` is an actor that holds and manages state values. /// -/// It is fully thread-safe as it is implemented using an actor. It stores the `State` and can -/// change the `State` by receiving `Actions`. You can define `Action` and `State` in ``Reducer``. -/// If you create a data flow through `Store`, you can make it flow in one direction. +/// It is fully thread-safe because it is implemented as an actor. It stores the `State` and can +/// change the `State` by receiving `Action`s. You can define `Action` and `State` in a +/// ``Reducer``. If you create a data flow through a `Store`, you can ensure that it flows in a +/// single direction. public actor Store> where R.Action: Sendable, R.State: Sendable & Equatable { - /// A convenience type alias for referring to a action of a given reducer's action. + /// A convenience type alias for referring to a given reducer's action. public typealias Action = R.Action - /// A convenience type alias for referring to a state of a given reducer's state. + /// A convenience type alias for referring to a given reducer's state. public typealias State = R.State private typealias TaskID = UUID - /// The initial state of a store. + /// The initial state of the store. public let initialState: State - /// The current state of a store. + /// The current state of the store. public private(set) var state: State { didSet { if oldValue != state { @@ -49,11 +50,14 @@ where R.Action: Sendable, R.State: Sendable & Equatable { } } - /// The state stream that emits state when the state changes. Use this stream to observe the - /// state changes. + /// The state stream that emits state changes. + /// + /// Use this stream to observe state changes. public var states: AsyncStream - /// Returns `true` if the store is idle, meaning it's not processing and there are no pending + /// A boolean value indicating whether the store is currently idle. + /// + /// A store is considered idle when it is not processing any actions and there are no pending /// tasks for side effects. public var isIdle: Bool { !isProcessing && tasks.isEmpty @@ -74,15 +78,15 @@ where R.Action: Sendable, R.State: Sendable & Equatable { private var throttleTimestamps: [EffectIDWrapper: C.Instant] = [:] private var trailingThrottledEffects: [EffectIDWrapper: AnyEffect] = [:] - /// Initializes a store from a reducer, an initial state, and a clock. + /// Initializes a new store with a reducer, an initial state, and a clock. /// /// - Parameters: - /// - reducer: The reducer responsible for transitioning the current state to the next - /// state in response to actions. - /// - state: The initial state used to create the store. - /// - loggingOptions: A set of options for logging. Defaults to `none`. - /// - clock: The clock that determines how time-based effects (such as debounce or throttle) - /// are scheduled. Defaults to `ContinuousClock`. + /// - reducer: The reducer that is responsible for transitioning the current state to the + /// next state in response to actions. + /// - state: The initial state to be used for the store. + /// - loggingOptions: A set of options for logging. The default is `none`. + /// - clock: The clock that determines how time-based effects, such as debounce or + /// throttle, are scheduled. The default is `ContinuousClock`. public init( reducer: @Sendable @autoclosure () -> R, state: State, @@ -129,10 +133,10 @@ where R.Action: Sendable, R.State: Sendable & Equatable { isProcessing = false } - /// Removes all actions and effects in the queue and re-binds for global states. + /// Resets the store by removing all queued actions and effects and re-binding global states. /// - /// - Note: This is useful when you need to call `bind()` again. Because you can't call `bind()` - /// directly + /// - Note: This is useful when you need to call `bind()` again, as you cannot call `bind()` + /// directly. public func reset() { bindExternalEffect() tasks.forEach { $0.value.cancel() } @@ -150,10 +154,10 @@ where R.Action: Sendable, R.State: Sendable & Equatable { /// user interactions or when debugging a specific issue. /// /// ```swift - /// // Enable logging for both actions and state changes. + /// // Enables logging for both actions and state changes. /// await store.debug(.all) /// - /// // Disable all logging. + /// // Disables all logging. /// await store.debug(.none) /// ``` /// diff --git a/Sources/OneWay/ViewStore.swift b/Sources/OneWay/ViewStore.swift index 5e9c15e..99d3752 100644 --- a/Sources/OneWay/ViewStore.swift +++ b/Sources/OneWay/ViewStore.swift @@ -12,21 +12,21 @@ import Combine /// `ViewStore` is an object that manages state values within the context of the `MainActor`. /// -/// It can observe state changes and send actions. It can primarily be used in SwiftUI's `View`, -/// `UIView` or `UIViewController` operating on main thread. +/// It can be used to observe state changes and send actions. It is primarily intended for use in +/// SwiftUI's `View`, `UIView`, or `UIViewController`, all of which operate on the main thread. @MainActor public final class ViewStore> where R.Action: Sendable, R.State: Sendable & Equatable { - /// A convenience type alias for referring to a action of a given reducer's action. + /// A convenience type alias for referring to a given reducer's action. public typealias Action = R.Action - /// A convenience type alias for referring to a state of a given reducer's state. + /// A convenience type alias for referring to a given reducer's state. public typealias State = R.State - /// The initial state of a store. + /// The initial state of the store. public let initialState: State - /// The current state of a store. + /// The current state of the store. public private(set) var state: State { didSet { continuation.yield(state) @@ -37,22 +37,23 @@ where R.Action: Sendable, R.State: Sendable & Equatable { } } - /// The async stream that emits state when the state changes. Use this stream to observe the - /// state changes + /// The asynchronous stream that emits state changes. + /// + /// Use this stream to observe state changes. public let states: AsyncViewStateSequence private let store: Store private let continuation: AsyncStream.Continuation private var task: Task? - /// Initializes a view store from a reducer, an initial state, and a clock. + /// Initializes a new view store with a reducer, an initial state, and a clock. /// /// - Parameters: - /// - reducer: The reducer responsible for transitioning the current state to the next - /// state in response to actions. - /// - state: The initial state used to create the store. - /// - clock: The clock that determines how time-based effects (such as debounce or throttle) - /// are scheduled. Defaults to `ContinuousClock`. + /// - reducer: The reducer that is responsible for transitioning the current state to the + /// next state in response to actions. + /// - state: The initial state to be used for the store. + /// - clock: The clock that determines how time-based effects, such as debounce or + /// throttle, are scheduled. The default is `ContinuousClock`. public init( reducer: @Sendable @autoclosure () -> R, state: State, @@ -92,10 +93,10 @@ where R.Action: Sendable, R.State: Sendable & Equatable { } } - /// Removes all actions and effects in the queue and re-binds for global states. + /// Resets the store by removing all queued actions and effects and re-binding global states. /// - /// - Note: This is useful when you need to call `bind()` again. Because you can't call `bind()` - /// directly + /// - Note: This is useful when you need to call `bind()` again, as you cannot call `bind()` + /// directly. public func reset() { Task { @MainActor in await store.reset() @@ -109,14 +110,14 @@ where R.Action: Sendable, R.State: Sendable & Equatable { /// user interactions or when debugging a specific issue. /// /// ```swift - /// // Enable logging for both actions and state changes. + /// // Enables logging for both actions and state changes. /// @StateObject private var store = ViewStore( /// reducer: HomeReducer(), /// state: HomeReducer.State() /// ) /// .debug(.all) /// - /// // Disable all logging. + /// // Disables all logging. /// store.debug(.none) /// ``` /// @@ -139,14 +140,15 @@ import SwiftUI extension ViewStore { #if swift(>=6.0) - /// Creates a `Binding` that allows two-way data binding between a state value and an action. + /// Creates a `Binding` that allows for two-way data binding between a state value and an + /// action. /// /// - Parameters: /// - keyPath: A key path to access a specific value from the current state. /// - send: A closure that takes the updated value and returns an `Action` to be sent. /// - /// - Returns: A `Binding` object that allows reading from the state using the key path and - /// sending an action when the value is changed. + /// - Returns: A `Binding` object that allows for reading from the state using the key path + /// and sending an action when the value is changed. @inlinable public func binding( _ keyPath: KeyPath & Sendable, @@ -158,14 +160,15 @@ extension ViewStore { ) } #else - /// Creates a `Binding` that allows two-way data binding between a state value and an action. + /// Creates a `Binding` that allows for two-way data binding between a state value and an + /// action. /// /// - Parameters: /// - keyPath: A key path to access a specific value from the current state. /// - send: A closure that takes the updated value and returns an `Action` to be sent. /// - /// - Returns: A `Binding` object that allows reading from the state using the key path and - /// sending an action when the value is changed. + /// - Returns: A `Binding` object that allows for reading from the state using the key path + /// and sending an action when the value is changed. @inlinable public func binding( _ keyPath: KeyPath,