Skip to content

Commit 72769ac

Browse files
authored
Add a binding helper for easier SwiftUI integration (#102)
* Add a binding helper for easier SwiftUI integration * Update the documentation
1 parent f495cd5 commit 72769ac

File tree

4 files changed

+105
-3
lines changed

4 files changed

+105
-3
lines changed

README.md

+56
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,61 @@ for await number in store.states.number.removeDuplicates() {
185185
// Prints "10"
186186
```
187187

188+
### Integration with SwiftUI
189+
190+
It can be seamlessly integrated with [SwiftUI](https://developer.apple.com/documentation/swiftui).
191+
192+
```swift
193+
struct CounterView: View {
194+
@StateObject private var store = ViewStore(
195+
reducer: CountingReducer(),
196+
state: CountingReducer.State(number: 0)
197+
)
198+
199+
var body: some View {
200+
VStack {
201+
Text("\(store.state.number)")
202+
Toggle(
203+
"isLoading",
204+
isOn: Binding<Bool>(
205+
get: { store.state.isLoading },
206+
set: { store.send(.setIsLoading($0)) }
207+
)
208+
)
209+
}
210+
.onAppear {
211+
store.send(.increment)
212+
}
213+
}
214+
}
215+
```
216+
217+
There is also a helper function that makes it easy to create [Binding](https://developer.apple.com/documentation/swiftui/binding).
218+
219+
```swift
220+
struct CounterView: View {
221+
@StateObject private var store = ViewStore(
222+
reducer: CountingReducer(),
223+
state: CountingReducer.State(number: 0)
224+
)
225+
226+
var body: some View {
227+
VStack {
228+
Text("\(store.state.number)")
229+
Toggle(
230+
"isLoading",
231+
isOn: store.binding(\.isLoading, send: { .setIsLoading($0) })
232+
)
233+
}
234+
.onAppear {
235+
store.send(.increment)
236+
}
237+
}
238+
}
239+
```
240+
241+
For more details, please refer to the [examples](#examples).
242+
188243
### Cancelling Effects
189244

190245
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
311366
- [OneWayExample](https://github.com/DevYeom/OneWayExample)
312367
- [UIKit](https://github.com/DevYeom/OneWayExample/tree/main/CounterUIKit/Counter)
313368
- [SwiftUI](https://github.com/DevYeom/OneWayExample/tree/main/CounterSwiftUI/Counter)
369+
- [badabook-ios](https://github.com/OceanPositive/badabook-ios): A multi-platform application based on Clean Architecture.
314370

315371
## Requirements
316372

Sources/OneWay/AsyncSequences/AsyncViewStateSequence.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ where State: Sendable & Equatable {
6363
///
6464
/// - Parameter dynamicMember: a key path for the original state.
6565
/// - Returns: A new stream that has a part of the original state.
66-
#if swift(>=6)
66+
#if swift(>=6.0)
6767
public subscript<Property>(
6868
dynamicMember keyPath: KeyPath<State, Property> & Sendable
6969
) -> AsyncMapSequence<AsyncStream<State>, Property> {

Sources/OneWay/ViewStore.swift

+46
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,50 @@ where R.Action: Sendable, R.State: Sendable & Equatable {
103103
extension ViewStore: ObservableObject { }
104104
#endif
105105

106+
#if canImport(SwiftUI)
107+
import SwiftUI
108+
109+
extension ViewStore {
110+
#if swift(>=6.0)
111+
/// Creates a `Binding` that allows two-way data binding between a state value and an action.
112+
///
113+
/// - Parameters:
114+
/// - keyPath: A key path to access a specific value from the current state.
115+
/// - send: A closure that takes the updated value and returns an `Action` to be sent.
116+
///
117+
/// - Returns: A `Binding` object that allows reading from the state using the key path and
118+
/// sending an action when the value is changed.
119+
@inlinable
120+
public func binding<Value>(
121+
_ keyPath: KeyPath<State, Value> & Sendable,
122+
send: @MainActor @escaping (Value) -> Action
123+
) -> Binding<Value> {
124+
Binding(
125+
get: { self.state[keyPath: keyPath] },
126+
set: { self.send(send($0)) }
127+
)
128+
}
129+
#else
130+
/// Creates a `Binding` that allows two-way data binding between a state value and an action.
131+
///
132+
/// - Parameters:
133+
/// - keyPath: A key path to access a specific value from the current state.
134+
/// - send: A closure that takes the updated value and returns an `Action` to be sent.
135+
///
136+
/// - Returns: A `Binding` object that allows reading from the state using the key path and
137+
/// sending an action when the value is changed.
138+
@inlinable
139+
public func binding<Value>(
140+
_ keyPath: KeyPath<State, Value>,
141+
send: @MainActor @Sendable @escaping (Value) -> Action
142+
) -> Binding<Value> {
143+
Binding(
144+
get: { self.state[keyPath: keyPath] },
145+
set: { self.send(send($0)) }
146+
)
147+
}
148+
#endif
149+
}
150+
#endif
151+
106152
#endif

Sources/OneWayTesting/Store+Testing.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import XCTest
2121

2222
#if canImport(Testing)
2323
extension Store {
24-
#if swift(>=6)
24+
#if swift(>=6.0)
2525
/// Allows the expectation of a certain property value in the store's state. It compares the
2626
/// current value of the given `keyPath` in the state with an expected `input` value
2727
///
@@ -201,7 +201,7 @@ extension Store {
201201

202202
#if !canImport(Testing) && canImport(XCTest)
203203
extension Store {
204-
#if swift(>=6)
204+
#if swift(>=6.0)
205205
/// Allows the expectation of a certain property value in the store's state. It compares the
206206
/// current value of the given `keyPath` in the state with an expected `input` value
207207
///

0 commit comments

Comments
 (0)