Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Bool>(
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.
Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Property>(
dynamicMember keyPath: KeyPath<State, Property> & Sendable
) -> AsyncMapSequence<AsyncStream<State>, Property> {
Expand Down
46 changes: 46 additions & 0 deletions Sources/OneWay/ViewStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Value>(
_ keyPath: KeyPath<State, Value> & Sendable,
send: @MainActor @escaping (Value) -> Action
) -> Binding<Value> {
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<Value>(
_ keyPath: KeyPath<State, Value>,
send: @MainActor @Sendable @escaping (Value) -> Action
) -> Binding<Value> {
Binding(
get: { self.state[keyPath: keyPath] },
set: { self.send(send($0)) }
)
}
#endif
}
#endif

#endif
4 changes: 2 additions & 2 deletions Sources/OneWayTesting/Store+Testing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
///
Expand Down Expand Up @@ -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
///
Expand Down