diff --git a/README.md b/README.md index f30f201..5e7aad8 100644 --- a/README.md +++ b/README.md @@ -185,6 +185,61 @@ for await number in store.states.number.removeDuplicates() { // Prints "10" ``` +### Integration with SwiftUI + +It can be seamlessly integrated with [SwiftUI](https://developer.apple.com/documentation/swiftui). + +```swift +struct CounterView: View { + @StateObject private var store = ViewStore( + reducer: CountingReducer(), + state: CountingReducer.State(number: 0) + ) + + var body: some View { + VStack { + Text("\(store.state.number)") + Toggle( + "isLoading", + isOn: Binding( + get: { store.state.isLoading }, + set: { store.send(.setIsLoading($0)) } + ) + ) + } + .onAppear { + store.send(.increment) + } + } +} +``` + +There is also a helper function that makes it easy to create [Binding](https://developer.apple.com/documentation/swiftui/binding). + +```swift +struct CounterView: View { + @StateObject private var store = ViewStore( + reducer: CountingReducer(), + state: CountingReducer.State(number: 0) + ) + + var body: some View { + VStack { + Text("\(store.state.number)") + Toggle( + "isLoading", + isOn: store.binding(\.isLoading, send: { .setIsLoading($0) }) + ) + } + .onAppear { + store.send(.increment) + } + } +} +``` + +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. @@ -311,6 +366,7 @@ To learn how to use **OneWay** in more detail, go through the [documentation](ht - [OneWayExample](https://github.com/DevYeom/OneWayExample) - [UIKit](https://github.com/DevYeom/OneWayExample/tree/main/CounterUIKit/Counter) - [SwiftUI](https://github.com/DevYeom/OneWayExample/tree/main/CounterSwiftUI/Counter) +- [badabook-ios](https://github.com/OceanPositive/badabook-ios): A multi-platform application based on Clean Architecture. ## Requirements diff --git a/Sources/OneWay/AsyncSequences/AsyncViewStateSequence.swift b/Sources/OneWay/AsyncSequences/AsyncViewStateSequence.swift index 5040fbc..b1b25e1 100644 --- a/Sources/OneWay/AsyncSequences/AsyncViewStateSequence.swift +++ b/Sources/OneWay/AsyncSequences/AsyncViewStateSequence.swift @@ -63,7 +63,7 @@ where State: Sendable & Equatable { /// /// - Parameter dynamicMember: a key path for the original state. /// - Returns: A new stream that has a part of the original state. - #if swift(>=6) + #if swift(>=6.0) public subscript( dynamicMember keyPath: KeyPath & Sendable ) -> AsyncMapSequence, Property> { diff --git a/Sources/OneWay/ViewStore.swift b/Sources/OneWay/ViewStore.swift index 5fd7c9f..5c40f0f 100644 --- a/Sources/OneWay/ViewStore.swift +++ b/Sources/OneWay/ViewStore.swift @@ -103,4 +103,50 @@ where R.Action: Sendable, R.State: Sendable & Equatable { extension ViewStore: ObservableObject { } #endif +#if canImport(SwiftUI) +import SwiftUI + +extension ViewStore { + #if swift(>=6.0) + /// Creates a `Binding` that allows 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. + @inlinable + public func binding( + _ keyPath: KeyPath & Sendable, + send: @MainActor @escaping (Value) -> Action + ) -> Binding { + Binding( + get: { self.state[keyPath: keyPath] }, + set: { self.send(send($0)) } + ) + } + #else + /// Creates a `Binding` that allows 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. + @inlinable + public func binding( + _ keyPath: KeyPath, + send: @MainActor @Sendable @escaping (Value) -> Action + ) -> Binding { + Binding( + get: { self.state[keyPath: keyPath] }, + set: { self.send(send($0)) } + ) + } + #endif +} +#endif + #endif diff --git a/Sources/OneWayTesting/Store+Testing.swift b/Sources/OneWayTesting/Store+Testing.swift index 1538791..9d56de0 100644 --- a/Sources/OneWayTesting/Store+Testing.swift +++ b/Sources/OneWayTesting/Store+Testing.swift @@ -21,7 +21,7 @@ import XCTest #if canImport(Testing) extension Store { - #if swift(>=6) + #if swift(>=6.0) /// Allows the expectation of a certain property value in the store's state. It compares the /// current value of the given `keyPath` in the state with an expected `input` value /// @@ -201,7 +201,7 @@ extension Store { #if !canImport(Testing) && canImport(XCTest) extension Store { - #if swift(>=6) + #if swift(>=6.0) /// Allows the expectation of a certain property value in the store's state. It compares the /// current value of the given `keyPath` in the state with an expected `input` value ///