OneWayKit is a reactive unidirectional architecture library built with Combine. It allows you to update the state based on user actions and reflect those changes in the view seamlessly. The library is designed to be simple and easy to integrate into any feature of your application.
ViewFeature defines four key components:
- State: Represents the state of the view.
- Action: Captures user interactions.
- Updater: Updates the state based on actions.
- Middleware: Handles asynchronous operations in between.
struct TimerFeature: ViewFeature {
struct State: ViewState {
var currentTime: Float = 0
var isStarted: Bool = false
var interval: TimeInterval = 0.1
}
enum Action: ViewAction {
case start
case add
case tapRightButton
case toggleStart
}
static var updater: Updater = { state, action in
var newState = state
switch action {
case .add:
newState.currentTime += Float(state.interval)
case .toggleStart:
newState.isStarted.toggle()
default: break
}
return newState
}
static var middlewares: [Middleware]? = [TimerMiddleware()]
}
Middleware plays a key role in handling asynchronous tasks by processing actions from a specific feature and emitting desired actions. Using Combine, it can also emit periodic actions, and subscriptions can be canceled using the cancel(for:)
method.
final class TimerMiddleware: Middleware {
func send(_ action: ViewAction, currentState: any ViewState) -> AnyPublisher<ViewAction, Never> {
guard let currentState = currentState as? TimerFeature.State else {
return Empty().eraseToAnyPublisher()
}
switch action as? TimerFeature.Action {
case .start:
return Timer.publish(every: currentState.interval, on: .main, in: RunLoop.Mode.common)
.autoconnect()
.map { _ in
TimerFeature.Action.add
}
.eraseToAnyPublisher()
case .tapRightButton:
if currentState.isStarted {
return Publishers.Merge(
Just(TimerFeature.Action.cancel(for: TimerFeature.Action.start)),
Just(TimerFeature.Action.toggleStart)
)
.eraseToAnyPublisher()
} else {
return Publishers.Merge(
Just(TimerFeature.Action.start),
Just(TimerFeature.Action.toggleStart)
)
.eraseToAnyPublisher()
}
default:
return Empty().eraseToAnyPublisher()
}
}
}
2. Now, we use the previously defined ViewFeature to create a OneWay that represents the unidirectional flow of the view and set the initial state.
final class TimerViewController: UIViewController {
private let oneway = OneWay<TimerFeature>(initialState: .init())
...
private func setupOneWay() {
oneway.statePublisher
.map { String(format: "%.1f", $0.currentTime) }
.assign(to: \.text, on: timeLabel)
.store(in: &cancellables)
oneway.statePublisher
.map { $0.isStarted }
.removeDuplicates()
.sink { [weak self] isStarted in
self?.navigationItem.rightBarButtonItem?.title = isStarted ? "Stop" : "Start"
}
.store(in: &cancellables)
}
@objc private func tapLeftButton() {
oneway.send(.left)
}
@objc private func tapRightButton() {
oneway.send(.right)
}
@objc private func tapUpButton() {
oneway.send(.up)
}
@objc private func tapDownButton() {
oneway.send(.down)
}
When creating onewaykit, set the context first.
private lazy var oneway = OneWay<TracerFeature>(
initialState:
.init(
position: .init(x: view.center.x, y: view.center.y),
size: .init(width: 100, height: 100)
),
context: TracerViewController.self
)
If you want to track actions and state changes, you can use shouldTrace to trace them when sending actions, as shown below:
@objc private func tapLeftButton() {
oneway.send(.left, shouldTrace: true)
}
By setting shouldTrace to true, you can observe events that capture the context, action, and how the state changes as a result, as shown below:
First, register the ViewFeature to be used globally and set the initial state.
GlobalOneWay.registerState(feature: GlobalFeature.self, initialState: .init())
Then, you can subscribe to and use the state of the desired global feature.
GlobalOneWay.state(feature: GlobalFeature.self)?
.map { $0.backgroundColor }
.sink { [weak self] in
self?.titleLabel.backgroundColor = $0
}
.store(in: &cancellables)
You can also send actions to the global feature, of course.
GlobalOneWay.send(feature: GlobalFeature.self, .setBackgroundColor(.white))
You can install OneWayKit via Swift Package Manager by adding the following line to your Package.swift
:
import PackageDescription
let package = Package(
[...]
dependencies: [
.package(url: "https: github.com/fomagran/OneWayKit", from: "1.2.0"),
]
)
The following projects have greatly inspired the creation of OneWayKit.
Fomagran, [email protected]
This library is released under the MIT license. See LICENSE for details.