A library for reactive and unidirectional Swift applications
ReactorKit is a framework for a reactive and unidirectional Swift application architecture. This repository introduces the basic concept of ReactorKit and describes how to build an application using ReactorKit.
You may want to see the Examples section first if you’d like to see the actual code. For an overview of ReactorKit’s features and the reasoning behind its creation, you may also check the slides from this introductory presentation over at SlideShare.
ReactorKit is a combination of Flux and Reactive Programming. The user actions and the view states are delivered to each layer via observable streams. These streams are unidirectional: the view can only emit actions and the reactor can only emit states.
A View displays data. A view controller and a cell are treated as a view. The view binds user inputs to the action stream and binds the view states to each UI component. There’s no business logic in a view layer. A view just defines how to map the action stream and the state stream.
To define a view, just have an existing class conform a protocol named View
. Then your class will have a property named reactor
automatically. This property is typically set outside of the view.
class ProfileViewController: UIViewController, View {
var disposeBag = DisposeBag()
}
profileViewController.reactor = UserViewReactor() // inject reactor
When the reactor
property has changed, bind(reactor:)
gets called. Implement this method to define the bindings of an action stream and a state stream.
func bind(reactor: ProfileViewReactor) {
// action (View -> Reactor)
refreshButton.rx.tap.map { Reactor.Action.refresh }
.bind(to: reactor.action)
.disposed(by: self.disposeBag)
// state (Reactor -> View)
reactor.state.map { $0.isFollowing }
.bind(to: followButton.rx.isSelected)
.disposed(by: self.disposeBag)
}
Use StoryboardView
protocol if you’re using a storyboard to initialize view controllers. Everything is same but the only difference is that the StoryboardView
performs a binding after the view is loaded.
let viewController = MyViewController()
viewController.reactor = MyViewReactor() // will not executes `bind(reactor:)` immediately
class MyViewController: UIViewController, StoryboardView {
func bind(reactor: MyViewReactor) {
// this is called after the view is loaded (viewDidLoad)
}
}
A Reactor is an UI-independent layer which manages the state of a view. The foremost role of a reactor is to separate control flow from a view. Every view has its corresponding reactor and delegates all logic to its reactor. A reactor has no dependency to a view, so it can be easily tested.
Conform to the Reactor
protocol to define a reactor. This protocol requires three types to be defined: Action
, Mutation
and State
. It also requires a property named initialState
.
class ProfileViewReactor: Reactor {
// represent user actions
enum Action {
case refreshFollowingStatus(Int)
case follow(Int)
}
// represent state changes
enum Mutation {
case setFollowing(Bool)
}
// represents the current view state
struct State {
var isFollowing: Bool = false
}
let initialState: State = State()
}
An Action
represents a user interaction and State
represents a view state. Mutation
is a bridge between Action
and State
. A reactor converts the action stream to the state stream in two steps: mutate()
and reduce()
.
mutate()
mutate()
receives an Action
and generates an Observable<Mutation>
.
func mutate(action: Action) -> Observable<Mutation>
Every side effect, such as an async operation or API call, is performed in this method.
func mutate(action: Action) -> Observable<Mutation> {
switch action {
case let .refreshFollowingStatus(userID): // receive an action
return UserAPI.isFollowing(userID) // create an API stream
.map { (isFollowing: Bool) -> Mutation in
return Mutation.setFollowing(isFollowing) // convert to Mutation stream
}
case let .follow(userID):
return UserAPI.follow()
.map { _ -> Mutation in
return Mutation.setFollowing(true)
}
}
}
reduce()
reduce()
generates a new State
from a previous State
and a Mutation
.
func reduce(state: State, mutation: Mutation) -> State
This method is a pure function. It should just return a new State
synchronously. Don’t perform any side effects in this function.
func reduce(state: State, mutation: Mutation) -> State {
var state = state // create a copy of the old state
switch mutation {
case let .setFollowing(isFollowing):
state.isFollowing = isFollowing // manipulate the state, creating a new state
return state // return the new state
}
}
transform()
transform()
transforms each stream. There are three transform()
functions:
func transform(action: Observable<Action>) -> Observable<Action>
func transform(mutation: Observable<Mutation>) -> Observable<Mutation>
func transform(state: Observable<State>) -> Observable<State>
Implement these methods to transform and combine with other observable streams. For example, transform(mutation:)
is the best place for combining a global event stream to a mutation stream. See the Global States section for details.
These methods can be also used for debugging purposes:
func transform(action: Observable<Action>) -> Observable<Action> {
return action.debug("action") // Use RxSwift's debug() operator
}
Unlike Redux, ReactorKit doesn’t define a global app state. It means that you can use anything to manage a global state. You can use a BehaviorSubject
, a PublishSubject
or even a reactor. ReactorKit doesn’t force to have a global state so you can use ReactorKit in a specific feature in your application.
There is no global state in the Action → Mutation → State flow. You should use transform(mutation:)
to transform the global state to a mutation. Let’s assume that we have a global BehaviorSubject
which stores the current authenticated user. If you’d like to emit a Mutation.setUser(User?)
when the currentUser
is changed, you can do as following:
var currentUser: BehaviorSubject<User> // global state
func transform(mutation: Observable<Mutation>) -> Observable<Mutation> {
return Observable.merge(mutation, currentUser.map(Mutation.setUser))
}
Then the mutation will be emitted each time the view sends an action to a reactor and the currentUser
is changed.
You must be familiar with callback closures or delegate patterns for communicating between multiple views. ReactorKit recommends you to use reactive extensions for it. The most common example of ControlEvent
is UIButton.rx.tap
. The key concept is to treat your custom views as UIButton or UILabel.
Let’s assume that we have a ChatViewController
which displays messages. The ChatViewController
owns a MessageInputView
. When an user taps the send button on the MessageInputView
, the text will be sent to the ChatViewController
and ChatViewController
will bind in to the reactor’s action. This is an example MessageInputView
’s reactive extension:
extension Reactive where Base: MessageInputView {
var sendButtonTap: ControlEvent<String> {
let source = base.sendButton.rx.tap.withLatestFrom(...)
return ControlEvent(events: source)
}
}
You can use that extension in the ChatViewController
. For example:
messageInputView.rx.sendButtonTap
.map(Reactor.Action.send)
.bind(to: reactor.action)
ReactorKit has a built-in functionality for a testing. You’ll be able to easily test both a view and a reactor with a following instruction.
First of all, you have to decide what to test. There are two things to test: a view and a reactor.
A view can be tested with a stub reactor. A reactor has a property stub
which can log actions and force change states. If a reactor’s stub is enabled, both mutate()
and reduce()
are not executed. A stub has these properties:
var state: StateRelay<Reactor.State> { get }
var action: ActionSubject<Reactor.Action> { get }
var actions: [Reactor.Action] { get } // recorded actions
Here are some example test cases:
func testAction_refresh() {
// 1. prepare a stub reactor
let reactor = MyReactor()
reactor.isStubEnabled = true
// 2. prepare a view with a stub reactor
let view = MyView()
view.reactor = reactor
// 3. send an user interaction programmatically
view.refreshControl.sendActions(for: .valueChanged)
// 4. assert actions
XCTAssertEqual(reactor.stub.actions.last, .refresh)
}
func testState_isLoading() {
// 1. prepare a stub reactor
let reactor = MyReactor()
reactor.isStubEnabled = true
// 2. prepare a view with a stub reactor
let view = MyView()
view.reactor = reactor
// 3. set a stub state
reactor.stub.state.value = MyReactor.State(isLoading: true)
// 4. assert view properties
XCTAssertEqual(view.activityIndicator.isAnimating, true)
}
A reactor can be tested independently.
func testIsBookmarked() {
let reactor = MyReactor()
reactor.action.onNext(.toggleBookmarked)
XCTAssertEqual(reactor.currentState.isBookmarked, true)
reactor.action.onNext(.toggleBookmarked)
XCTAssertEqual(reactor.currentState.isBookmarked, false)
}
Sometimes a state is changed more than one time for a single action. For example, a .refresh
action sets state.isLoading
to true
at first and sets to false
after the refreshing. In this case it’s difficult to test state.isLoading
with currentState
so you might need to use RxTest or RxExpect. Here is an example test case using RxSwift:
func testIsLoading() {
// given
let scheduler = TestScheduler(initialClock: 0)
let reactor = MyReactor()
let disposeBag = DisposeBag()
// when
scheduler
.createHotObservable([
.next(100, .refresh) // send .refresh at 100 scheduler time
])
.subscribe(reactor.action)
.disposed(by: disposeBag)
// then
let response = scheduler.start(created: 0, subscribed: 0, disposed: 1000) {
reactor.state.map(\.isLoading)
}
XCTAssertEqual(response.events.map(\.value.element), [
false, // initial state
true, // just after .refresh
false // after refreshing
])
}
Pulse
has diff only when mutated
To explain in code, the results are as follows.
var messagePulse: Pulse<String?> = Pulse(wrappedValue: "Hello tokijh")
let oldMessagePulse: Pulse<String?> = messagePulse
messagePulse.value = "Hello tokijh" // add valueUpdatedCount +1
oldMessagePulse.valueUpdatedCount != messagePulse.valueUpdatedCount // true
oldMessagePulse.value == messagePulse.value // true
Use when you want to receive an event only if the new value is assigned, even if it is the same value.
like alertMessage
(See follows or PulseTests.swift)
// Reactor
private final class MyReactor: Reactor {
struct State {
@Pulse var alertMessage: String?
}
func mutate(action: Action) -> Observable<Mutation> {
switch action {
case let .alert(message):
return Observable.just(Mutation.setAlertMessage(message))
}
}
func reduce(state: State, mutation: Mutation) -> State {
var newState = state
switch mutation {
case let .setAlertMessage(alertMessage):
newState.alertMessage = alertMessage
}
return newState
}
}
// View
reactor.pulse(\.$alertMessage)
.compactMap { $0 } // filter nil
.subscribe(onNext: { [weak self] (message: String) in
self?.showAlert(message)
})
.disposed(by: disposeBag)
// Cases
reactor.action.onNext(.alert("Hello")) // showAlert() is called with `Hello`
reactor.action.onNext(.alert("Hello")) // showAlert() is called with `Hello`
reactor.action.onNext(.doSomeAction) // showAlert() is not called
reactor.action.onNext(.alert("Hello")) // showAlert() is called with `Hello`
reactor.action.onNext(.alert("tokijh")) // showAlert() is called with `tokijh`
reactor.action.onNext(.doSomeAction) // showAlert() is not called
Podfile
pod 'ReactorKit'
Package.swift
let package = Package(
name: "MyPackage",
dependencies: [
.package(url: "https://github.com/ReactorKit/ReactorKit.git", .upToNextMajor(from: "3.0.0"))
],
targets: [
.target(name: "MyTarget", dependencies: ["ReactorKit"])
]
)
ReactorKit does not officially support Carthage.
Cartfile
github "ReactorKit/ReactorKit"
Most Carthage installation issues can be resolved with the following:
carthage update 2>/dev/null
(cd Carthage/Checkouts/ReactorKit && swift package generate-xcodeproj)
carthage build
Any discussions and pull requests are welcomed 💖
To development:
TEST=1 swift package generate-xcodeproj
To test:
swift test
Are you using ReactorKit? Please let me know!
ReactorKit is under MIT license. See the LICENSE for more info.