AsyncReactorKit is a modern reactive architecture library for Swift applications that brings unidirectional data flow to the async/await era. Inspired by ReactorKit, it leverages Swift Concurrency instead of RxSwift to provide a more straightforward and type-safe reactive programming experience.
AsyncReactorKit reimagines reactive architecture for modern Swift development:
- Swift Concurrency First: Built from the ground up with async/await patterns
- Type Safety: Full Swift 6 concurrency compliance with
Sendableguarantees - Unidirectional Data Flow: Clear separation between inputs, mutations, and state
- Zero Dependencies: Uses custom
DynamicTypeDictionaryinstead of external reactive frameworks - Easy Testing: Built-in testing utilities with
Stubfor isolated unit tests
| Feature | ReactorKit | AsyncReactorKit |
|---|---|---|
| Concurrency | RxSwift Observables | Swift async/await |
| State Storage | WeakMapTable | DynamicTypeDictionary |
| Swift Version | Swift 5+ | Swift 6+ |
| Dependencies | RxSwift, RxCocoa | None |
| Thread Safety | Manual synchronization | Built-in Sendable compliance |
| Learning Curve | RxSwift knowledge required | Native Swift Concurrency |
Add AsyncReactorKit to your project using Xcode or by adding it to your Package.swift:
dependencies: [
.package(url: "https://github.com/yourusername/AsyncReactorKit.git", from: "1.0.0")
]- iOS 13.0+ / macOS 11.0+ / macCatalyst 13.0+
- Swift 6.0+
- Xcode 16.0+
import AsyncReactor
class CounterReactor: AsyncReactor {
enum Input {
case increment
case decrement
case reset
}
enum Mutation {
case setCount(Int)
}
struct State: Sendable {
@Pulse var count: Int = 0
@Pulse var message: String?
}
nonisolated func initialState() -> State {
State()
}
func reduce(_ input: Input) async -> Mutation {
switch input {
case .increment:
return .setCount(currentState.count + 1)
case .decrement:
return .setCount(currentState.count - 1)
case .reset:
return .setCount(0)
}
}
func mutate(_ state: State, mutation: Mutation) async -> State {
var newState = state
switch mutation {
case .setCount(let count):
newState.count = count
newState.message = "Count updated to \(count)"
}
return newState
}
}import UIKit
import AsyncReactor
class CounterViewController: UIViewController, Store {
typealias Reactor = CounterReactor
@IBOutlet weak var countLabel: UILabel!
@IBOutlet weak var incrementButton: UIButton!
@IBOutlet weak var decrementButton: UIButton!
var reactor: CounterReactor?
override func viewDidLoad() {
super.viewDidLoad()
reactor = CounterReactor()
setupActions()
}
private func setupActions() {
incrementButton.addTarget(self, action: #selector(incrementTapped), for: .touchUpInside)
decrementButton.addTarget(self, action: #selector(decrementTapped), for: .touchUpInside)
}
@objc private func incrementTapped() {
send(.increment)
}
@objc private func decrementTapped() {
send(.decrement)
}
// Store protocol implementation
func state(_ state: CounterReactor.State) {
state.$count.updated { [weak self] count in
self?.countLabel.text = "\(count)"
}
state.$message.updated { [weak self] message in
guard let message = message else { return }
self?.showAlert(message)
}
}
}
⚠️ Note: SwiftUI integration is currently experimental and not officially supported. The API will be improved in future updates for better SwiftUI compatibility. The following example demonstrates a temporary workaround pattern.
import SwiftUI
import AsyncReactor
@MainActor
class CounterStore: ObservableObject, Store {
typealias Reactor = CounterReactor
@Published private(set) var currentState = CounterReactor.State()
var reactor: CounterReactor?
init() {
reactor = CounterReactor()
}
func state(_ state: CounterReactor.State) {
currentState = state
}
}
struct CounterView: View {
@StateObject private var store = CounterStore()
var body: some View {
VStack(spacing: 20) {
Text("\(store.currentState.count)")
.font(.largeTitle)
HStack {
Button("−") { store.send(.decrement) }
Button("+") { store.send(.increment) }
Button("Reset") { store.send(.reset) }
}
}
.padding()
}
}The heart of the architecture, defining the reactive data flow:
protocol AsyncReactor: AnyObject {
associatedtype Input: Sendable
associatedtype Mutation: Sendable
associatedtype State: Sendable
nonisolated func initialState() -> State
func reduce(_ input: Input) async -> Mutation
func mutate(_ state: State, mutation: Mutation) async -> State
}Connects UI components to reactors:
protocol Store: AnyObject {
associatedtype Reactor: AsyncReactor
var reactor: Reactor? { get }
func state(_ state: Reactor.State)
}A property wrapper for state properties that trigger updates on every assignment:
struct State {
@Pulse var alertMessage: String? // Triggers even with same value
var userData: User? // Regular property
}func testIncrement() async {
let reactor = CounterReactor()
let stub = Stub(reactor: reactor)
let newState = await stub.test(input: .increment)
XCTAssertEqual(newState.count, 1)
}User Action → Input → reduce() → Mutation → mutate() → State → UI Update
- Input: User actions or external events
- reduce(): Process inputs and handle side effects (API calls, etc.)
- Mutation: Atomic state changes
- mutate(): Apply mutations to create new state
- State: Single source of truth for UI
- All types must conform to
Sendable - Automatic concurrency safety with internal synchronization
- Main thread UI updates handled automatically
- Stub: Test reactors in isolation
- Predictable: Pure functions make testing straightforward
- Fast: No UI dependencies required
Check out the complete example in the Example/ directory:
- CounterApp: Basic counter with increment/decrement
- Advanced Features: Pulse usage, async operations, testing
cd Example/AsyncReactorKitSample
open AsyncReactorKitSample.xcodeproj| ReactorKit | AsyncReactorKit |
|---|---|
Action |
Input |
reactor.action.onNext(.action) |
store.send(.input) |
reactor.state.map(\.property) |
state.$property.updated { } |
Observable<State> |
Direct state updates via Store |
DisposeBag |
Not needed (automatic cleanup) |
AsyncReactorKit uses a minimal dependency:
- DynamicTypeDictionary: Type-safe storage solution replacing WeakMapTable
AsyncReactorKit is available under the MIT license.
- ReactorKit: The original RxSwift-based reactive architecture
- DynamicTypeDictionary: Type-safe dictionary for dynamic type storage
Made with ❤️ for the Swift community