- Notifications
You must be signed in to change notification settings - Fork274
A library for reactive and unidirectional Swift applications
License
ReactorKit/ReactorKit
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
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 theExamples 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 atSlideShare.
- Table of Contents
- Basic Concept
- Advanced
- Examples
- Dependencies
- Requirements
- Installation
- Contribution
- Community
- Who's using ReactorKit
- Changelog
- License
ReactorKit is a combination ofFlux andReactive 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.
- Testability: The first purpose of ReactorKit is to separate the business logic from a view. This can make the code testable. A reactor doesn't have any dependency to a view. Just test reactors and test view bindings. SeeTesting section for details.
- Start Small: ReactorKit doesn't require the whole application to follow a single architecture. ReactorKit can be adopted partially, for one or more specific views. You don't need to rewrite everything to use ReactorKit on your existing project.
- Less Typing: ReactorKit focuses on avoiding complicated code for a simple thing. ReactorKit requires less code compared to other architectures. Start simple and scale up.
AView 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 namedReactorView. Then your class will have a property namedreactor automatically. This property is typically set outside of the view.
classProfileViewController:UIViewController,ReactorView{vardisposeBag=DisposeBag()}profileViewController.reactor=UserViewReactor() // inject reactor
When thereactor 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)}
UseDeferredReactorView protocol if you're using a storyboard to initialize view controllers. Everything is same but the only difference is that theDeferredReactorView defers binding until the view is loaded.
letviewController=MyViewController()viewController.reactor=MyViewReactor() // will not execute `bind(reactor:)` immediatelyclassMyViewController:UIViewController,DeferredReactorView{func bind(reactor:MyViewReactor){ // this is called after the view is loaded (viewDidLoad)}}
AReactor 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 theReactor protocol to define a reactor. This protocol requires three types to be defined:Action,Mutation andState. It also requires a property namedinitialState.
classProfileViewReactor:Reactor{ // represent user actionsenumAction{case refreshFollowingStatus(Int)case follow(Int)} // represent state changesenumMutation{case setFollowing(Bool)} // represents the current view statestructState{varisFollowing:Bool=false}letinitialState:State=State()}
AnAction represents a user interaction andState represents a view state.Mutation is a bridge betweenAction andState. A reactor converts the action stream to the state stream in two steps:mutate() andreduce().
mutate() receives anAction and generates anObservable<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{caselet.refreshFollowingStatus(userID): // receive an actionreturnUserAPI.isFollowing(userID) // create an API stream.map{(isFollowing:Bool)->MutationinreturnMutation.setFollowing(isFollowing) // convert to Mutation stream}caselet.follow(userID):returnUserAPI.follow().map{ _->MutationinreturnMutation.setFollowing(true)}}}
reduce() generates a newState from a previousState and aMutation.
func reduce(state:State, mutation:Mutation)->State
This method is a pure function. It should just return a newState synchronously. Don't perform any side effects in this function.
func reduce(state:State, mutation:Mutation)->State{varstate= state // create a copy of the old stateswitch mutation{caselet.setFollowing(isFollowing): state.isFollowing= isFollowing // manipulate the state, creating a new statereturn state // return the new state}}
transform() transforms each stream. There are threetransform() 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 theGlobal 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 aBehaviorSubject, aPublishSubject 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 theAction → Mutation → State flow. You should usetransform(mutation:) to transform the global state to a mutation. Let's assume that we have a globalBehaviorSubject which stores the current authenticated user. If you'd like to emit aMutation.setUser(User?) when thecurrentUser is changed, you can do as following:
varcurrentUser:BehaviorSubject<User> // global statefunc transform(mutation:Observable<Mutation>)->Observable<Mutation>{returnObservable.merge(mutation, currentUser.map(Mutation.setUser))}
Then the mutation will be emitted each time the view sends an action to a reactor and thecurrentUser is changed.
You must be familiar with callback closures or delegate patterns for communicating between multiple views. ReactorKit recommends you to usereactive extensions for it. The most common example ofControlEvent isUIButton.rx.tap. The key concept is to treat your custom views as UIButton or UILabel.
Let's assume that we have aChatViewController which displays messages. TheChatViewController owns aMessageInputView. When an user taps the send button on theMessageInputView, the text will be sent to theChatViewController andChatViewController will bind in to the reactor's action. This is an exampleMessageInputView's reactive extension:
extensionReactivewhere Base:MessageInputView{varsendButtonTap:ControlEvent<String>{letsource= base.sendButton.rx.tap.withLatestFrom(...)returnControlEvent(events: source)}}
You can use that extension in theChatViewController. 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.
- View
- Action: is a proper action sent to a reactor with a given user interaction?
- State: is a view property set properly with a following state?
- Reactor
- State: is a state changed properly with an action?
A view can be tested with astub reactor. A reactor has a propertystub which can log actions and force change states. If a reactor's stub is enabled, bothmutate() andreduce() are not executed. A stub has these properties:
varstate:StateRelay<Reactor.State>{get}varaction:ActionSubject<Reactor.Action>{get}varactions:[Reactor.Action]{get} // recorded actions
Here are some example test cases:
func testAction_refresh(){ // 1. prepare a stub reactorletreactor=MyReactor() reactor.isStubEnabled=true // 2. prepare a view with a stub reactorletview=MyView() view.reactor= reactor // 3. send an user interaction programmatically view.refreshControl.sendActions(for:.valueChanged) // 4. assert actionsXCTAssertEqual(reactor.stub.actions.last,.refresh)}func testState_isLoading(){ // 1. prepare a stub reactorletreactor=MyReactor() reactor.isStubEnabled=true // 2. prepare a view with a stub reactorletview=MyView() view.reactor= reactor // 3. set a stub state reactor.stub.state.value=MyReactor.State(isLoading:true) // 4. assert view propertiesXCTAssertEqual(view.activityIndicator.isAnimating,true)}
A reactor can be tested independently.
func testIsBookmarked(){letreactor=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 setsstate.isLoading totrue at first and sets tofalse after the refreshing. In this case it's difficult to teststate.isLoading withcurrentState so you might need to useRxTest orRxExpect. Here is an example test case using RxSwift:
func testIsLoading(){ // givenletscheduler=TestScheduler(initialClock:0)letreactor=MyReactor()letdisposeBag=DisposeBag() // when scheduler.createHotObservable([.next(100,.refresh) // send .refresh at 100 scheduler time]).subscribe(reactor.action).disposed(by: disposeBag) // thenletresponse= scheduler.start(created:0, subscribed:0, disposed:1000){ reactor.state.map(\.isLoading)}XCTAssertEqual(response.events.map(\.value.element),[false, // initial statetrue, // just after .refreshfalse // after refreshing])}
Pulse has diff only when mutatedTo explain in code, the results are as follows.
varmessagePulse:Pulse<String?>=Pulse(wrappedValue:"Hello tokijh")letoldMessagePulse:Pulse<String?>= messagePulsemessagePulse.value="Hello tokijh" // add valueUpdatedCount +1oldMessagePulse.valueUpdatedCount!= messagePulse.valueUpdatedCount // trueoldMessagePulse.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.likealertMessage (See follows orPulseTests.swift)
// ReactorprivatefinalclassMyReactor:Reactor{structState{@PulsevaralertMessage:String?}func mutate(action:Action)->Observable<Mutation>{switch action{caselet.alert(message):returnObservable.just(Mutation.setAlertMessage(message))}}func reduce(state:State, mutation:Mutation)->State{varnewState= stateswitch mutation{caselet.setAlertMessage(alertMessage): newState.alertMessage= alertMessage}return newState}}// Viewreactor.pulse(\.$alertMessage).compactMap{ $0} // filter nil.subscribe(onNext:{[weak self](message:String)inself?.showAlert(message)}).disposed(by: disposeBag)// Casesreactor.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 calledreactor.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
- Counter: The most simple and basic example of ReactorKit
- GitHub Search: A simple application which provides a GitHub repository search
- RxTodo: iOS Todo Application using ReactorKit
- Cleverbot: iOS Messaging Application using Cleverbot and ReactorKit
- Drrrible: Dribbble for iOS using ReactorKit (App Store)
- Passcode: Passcode for iOS RxSwift, ReactorKit and IGListKit example
- Flickr Search: A simple application which provides a Flickr Photo search with RxSwift and ReactorKit
- ReactorKitExample
- reactorkit-keyboard-example: iOS Application example for develop keyboard-extensions using ReactorKit Architecture.
- TinyHub: Use ReactorKit develop the Github client
- RxSwift >= 5.0
- Swift 5
- iOS 8
- macOS 10.11
- tvOS 9.0
- watchOS 2.0
Podfile
pod'ReactorKit'
Package.swift
letpackage=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 update2>/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-xcodeprojTo test:
swift test
- English: Join#reactorkit onRxSwift Slack
- Korean: Join#reactorkit onSwift Korea Slack
Are you using ReactorKit? Pleaselet me know!
- 2017-04-18
- Change the repository name to ReactorKit.
- 2017-03-17
- Change the architecture name from RxMVVM to The Reactive Architecture.
- Every ViewModels are renamed to ViewReactors.
ReactorKit is under MIT license. See theLICENSE for more info.
About
A library for reactive and unidirectional Swift applications
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Packages0
Uh oh!
There was an error while loading.Please reload this page.














