OneWay is a remarkably simple and lightweight library designed for state management through unidirectional data flow. It is implemented based on Swift Concurrency. The Store
is implemented with an Actor
, making it always thread-safe.
Whether you're working on any platform or within any framework, OneWay can seamlessly integrate. With zero third-party dependencies, OneWay can be used in its purest form. This library isn't limited to use only for the presentation layer. It's also useful for streamlining intricate business logic. You'll find it beneficial whenever you seek to implement logic in a unidirectional manner.
When using the Store
, the data flow is as follows.
When working on UI, it is better to use ViewStore
to ensure main thread operation.
After adopting the Reducer
protocol, define the Action
and State
, and then implement the logic for each Action
within the reduce(state:action:)
function.
struct CountingReducer: Reducer {
enum Action: Sendable {
case increment
case decrement
case twice
case setIsLoading(Bool)
}
struct State: Sendable, Equatable {
var number: Int
var isLoading: Bool
}
func reduce(state: inout State, action: Action) -> AnyEffect<Action> {
switch action {
case .increment:
state.number += 1
return .none
case .decrement:
state.number -= 1
return .none
case .twice:
return .concat(
.just(.setIsLoading(true)),
.merge(
.just(.increment),
.just(.increment)
),
.just(.setIsLoading(false))
)
case .setIsLoading(let isLoading):
state.isLoading = isLoading
return .none
}
}
}
Sending an action to a Store causes changes in the state
via Reducer
.
let store = Store(
reducer: CountingReducer(),
state: CountingReducer.State(number: 0)
)
await store.send(.increment)
await store.send(.decrement)
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.
let store = ViewStore(
reducer: CountingReducer(),
state: CountingReducer.State(number: 0)
)
store.send(.increment)
store.send(.decrement)
store.send(.twice)
print(store.state.number) // 2
When the state changes, you can receive a new state. It guarantees that the same state does not come down consecutively.
struct State: Sendable, Equatable {
var number: Int
}
// number <- 10, 10, 20 ,20
for await state in store.states {
print(state.number)
}
// Prints "10", "20"
Of course, you can observe specific properties only.
// number <- 10, 10, 20 ,20
for await number in store.states.number {
print(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, @Ignored), refer to here.
struct State: Sendable, Equatable {
@Triggered var number: Int
}
// number <- 10, 10, 20 ,20
for await state in store.states {
print(state.number)
}
// 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, you can remove duplicates as follows.
struct State: Sendable, Equatable {
var number: Int
var text: String
}
// number <- 10
// text <- "a", "b", "c"
for await number in store.states.number {
print(number)
}
// Prints "10", "10", "10"
for await number in store.states.number.removeDuplicates() {
print(number)
}
// Prints "10"
You can make an effect capable of being canceled by using cancellable()
. And you can use cancel()
to cancel a cancellable effect.
func reduce(state: inout State, action: Action) -> AnyEffect<Action> {
switch action {
// ...
case .request:
return .single {
let result = await api.result()
return Action.response(result)
}
.cancellable("requestID")
case .cancel:
return .cancel("requestID")
// ...
}
}
You can assign anything that conforms Hashable as an identifier for the effect, not just a string.
enum EffectID {
case request
}
func reduce(state: inout State, action: Action) -> AnyEffect<Action> {
switch action {
// ...
case .request:
return .single {
let result = await api.result()
return Action.response(result)
}
.cancellable(EffectID.request)
case .cancel:
return .cancel(EffectID.request)
// ...
}
}
OneWay supports various effects such as just
, concat
, merge
, single
, sequence
, and more. For more details, please refer to the documentation.
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
.
let textPublisher = PassthroughSubject<String, Never>()
let numberPublisher = PassthroughSubject<Int, Never>()
struct CountingReducer: Reducer {
// ...
func bind() -> AnyEffect<Action> {
return .merge(
.sequence { send in
for await text in textPublisher.values {
send(Action.response(text))
}
},
.sequence { send in
for await number in numberPublisher.values {
send(Action.response(String(number)))
}
}
)
}
// ...
}
To learn how to use OneWay in more detail, go through the documentation.
OneWay | Swift | Xcode | Platforms |
---|---|---|---|
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 |
OneWay is only supported by Swift Package Manager.
To integrate OneWay into your Xcode project using Swift Package Manager, add it to the dependencies value of your Package.swift
:
dependencies: [
.package(url: "https://github.com/DevYeom/OneWay", from: "2.0.0"),
]
These are the references that have provided much inspiration.
This library is released under the MIT license. See LICENSE for details.