16 KiB
RxSwift: ReactiveX for Swift
Hang out with us on rxswift.slack.com
Requirements
Xcode 7 / Swift 2.0 required
This README.md describes alpha version of RxSwift 2.0.
You can find RxSwift 1.9 for Swift 1.2 here.
We will be applying critical hotfixes to 1.9 version, but since the entire ecosystem is migrating towards Swift 2.0, we will be focusing on adding new features only to RxSwift 2.0 version.
We will support all environments where Swift 2.0 will run.
Project Structure
RxSwift
|
├-LICENSE.md
├-README.md
├-RxSwift - platform agnostic core
├-RxCocoa - extensions for UI, NSURLSession, KVO ...
├-RxBlocking - set of blocking operators for unit testing
├-RxExample - example apps: taste of Rx
└-Rx.xcworkspace - workspace that contains all of the projects hooked up
Light Intro to RxSwift
Rx, also called Reactive Extensions, is a generic abstraction of computation expressed through Observable<Element> interface.
RxSwift is a Swift version of Rx that tries to port as many concepts from the original version as possible, but some concepts were adapted for more pleasant integration with iOS/OSX environment and better performances.
You can find cross platform documentation on ReactiveX.io website.
Like the original Rx implementation, the intention of RxSwift is to enable simple composition of asynchronous operations and event/data streams. For example: KVO observing, async operations and streams are all unified under abstraction of sequence. This is the reason why Rx is so simple, elegant and powerful.
Quick Example
Less chat, more code, time to see some code: let's assume you have to write a typical autocomplete example.
If you were asked to write the autocomplete code without Rx, the very first problem that probably needs to be addressed is when c in abc is typed, this implies you have a pending request for ab that has to be canceled. Right, that shouldn't be too hard to solve, you just create additional variable to hold reference to a pending request and if a previous one is still processing, you cancel it.
The next problem is if the request fails, you then need to do that messy retry logic. Again, a couple of more fields that capture number of retries that need to be cleaned up.
Then, because your code fires a request on every single character typed, you receive a call from the backend guys complaining because the application is spamming the server with an unreasonable amount of requests when users are typing something long. A potential solution is a timer, isn't it?
When you thought you finally nailed it, another problem arise, what needs to be shown on screen while that search is executing? Also what needs to be shown in case we fail even with all of the retries? Last but not least, what about properly testing this code?
Assuming your head is definitely full of potential implementations, but also questions and that the final code would look like Spaghetti code, this is the exactly same logic written with RxSwift:
searchTextField.rx_text
.throttle(0.3, MainScheduler.sharedInstance)
.distinctUntilChanged()
.map { query in
API.getSearchResults(query)
.retry(3)
.startWith([]) // clears results on new search term
.catchErrorJustReturn([])
}
.switchLatest()
.subscribeNext { results in
// bind to ui
}
Guess what? There's no addition for flags or fields required. Rx takes care of all that transient mess for you.
Benefits of using Rx
As the previous example demonstrated, using Rx will make your code:
- composable <- because Rx is composition's nick name
- reusable <- because it's composable
- declarative <- because definitions are immutable and only data changes
- understandable and concise <- raising level of abstraction and removing transient states
- stable <- because Rx code is thoroughly unit tested
- less stateful <- because you are modeling application as unidirectional data flows
- without leaks <- because resource management is simple
Rx is not all or nothing
It is usually a good idea to model as much of your application as possible using Rx.
But what if you don't know all of the operators and does there even exist some operator that models your particular case?
Well, all of the Rx operators are based on math and should be intuitive.
The good news is that about 10-15 operators cover most typical use cases. And that list already includes some of the familiar ones like map, filter, zip, observeOn ...
There is a huge list of all Rx operators and list of all of the currently supported RxSwift operators.
For each operator there is marble diagram that helps to explain how does it work.
But what if you need some operator that isn't on that list? Well, you can make your own operator.
What if creating that kind of operator is really hard for some reason, or you have some legacy stateful piece of code that you need to work with? Well, you've got yourself in a mess, but you can jump out of Rx monad easily, process the data, and return back into it.
Type of Events
RxSwift is based on the concept of events, an Event is an enum defined in the following way:
enum Event<Element> {
case Next(Element) // next element of a sequence
case Error(ErrorType) // sequence failed with error
case Completed // sequence terminated successfully
}
This means we have three kind of events:
- Next: A new element is produced and the consumer (or subscription) should process it
- Error: An error occurred, at this point all the subscriptions are canceled and the sequence is terminated
- Complete: The sequence has terminated and all subscriptions can be canceled
Integration
The most common question is: how do I create an observable? It's pretty simple.
This code snippet is take directly from RxCocoa and that's all you need to wrap HTTP requests with NSURLSession:
extension NSURLSession {
public func rx_response(request: NSURLRequest) -> Observable<(NSData!, NSURLResponse!)> {
return create { observer in
let task = self.dataTaskWithRequest(request) { (data, response, error) in
if data == nil || response == nil {
observer.on(.Error(error ?? UnknownError))
}
else {
observer.on(.Next(data, response))
observer.on(.Completed)
}
}
task.resume()
return AnonymousDisposable {
task.cancel()
}
}
}
}
I would then be able to use it in this way:
NSURLSession.sharedSession().rx_response(request).subscribeNext() { data, response in
// process the result of a request
}
or if handling all events is necessary:
NSURLSession.sharedSession().rx_response(request).subscribe(next: { data, response in
// process the data
},
error: { e in
// process an error
},
completed: {
// finalize the sequence
},
disposed: {
// performed when a sequence is about to be disposed
})
Getting Started
Rx doesn't contain any external dependencies and supports OS X 10.9+ and iOS 7.0+.
Manual
Open Rx.xcworkspace, choose RxExample and hit run. This method will build everything and run sample app
CocoaPods
⚠️ IMPORTANT! For tvOS support through CocoaPods use this hack until 0.39 is released, or install the pre-release version with $ [sudo] gem install cocoapods --pre. ⚠️
# Podfile
use_frameworks!
pod 'RxSwift', '~> 2.0.0-alpha'
pod 'RxCocoa', '~> 2.0.0-alpha'
pod 'RxBlocking', '~> 2.0.0-alpha'
type in Podfile directory
$ pod install
Carthage
Add this to Cartfile
git "git@github.com:ReactiveX/RxSwift.git" "2.0.0-alpha.4"
$ carthage update
Manually using git submodules
- Add RxSwift as a submodule
$ git submodule add git@github.com:ReactiveX/RxSwift.git
- Drag
Rx.xcodeprojinto Project Navigator - Go to
Project > Targets > Build Phases > Link Binary With Libraries, click+and selectRxSwift-[Platform]andRxCocoa-[Platform]targets
If you require to link against iOS 7, please follow the dedicated guide .
Documentation
If you need more information about RxSwift, you can find it here:
- Why
- Getting started
- Examples
- API - RxSwift operators / RxCocoa extensions
- Math behind
- Hot and cold observables
- Feature comparison with other frameworks
- Roadmap
- Playgrounds
- RxExamples
- References
Feature comparison with other frameworks in the Swift's reactive space
| Rx[Swift] | ReactiveCocoa | Bolts | PromiseKit | |
|---|---|---|---|---|
| Language | swift | objc/swift | objc | objc/swift |
| Basic Concept | Sequence | Signal SignalProducer | Task | Promise |
| Cancellation | • | • | • | • |
| Async operations | • | • | • | • |
| map/filter/... | • | • | • | |
| cache invalidation | • | • | ||
| cross platform | • | • | ||
| blocking operators for unit testing | • | N/A | N/A | |
| Lockless single sequence operators (map, filter, ...) | • | N/A | N/A | |
| Unified hot and cold observables | • | N/A | N/A | |
| RefCount | • | N/A | N/A | |
| Concurrent schedulers | • | N/A | N/A | |
| Generated optimized narity operators (combineLatest, zip) | • | N/A | N/A | |
| Reentrant operators | • | N/A | N/A |
** Comparison with RAC with respect to v3.0-RC.1
Playgrounds
To use playgrounds:
- Open
Rx.xcworkspace - Build
RxSwift-OSXscheme - And then open
Rxplayground inRx.xcworkspacetree view. - Choose
View > Show Debug Area
RxExamples
To use playgrounds:
- Open
Rx.xcworkspace - Choose one of example schemes and hit
Run.
References
- http://reactivex.io/
- Reactive Extensions GitHub (GitHub)
- Erik Meijer (Wikipedia)
- Expert to Expert: Brian Beckman and Erik Meijer - Inside the .NET Reactive Framework (Rx) (video)
- Subject/Observer is Dual to Iterator (paper)
- Rx standard sequence operators visualized (visualization tool)
- Haskell
Change Log (from 1.9 version)
- Removes deprecated APIs
- Adds
ObservableType - Moved from using
>-operator to protocol extensions. - Adds support for Swift 2.0 error handling
try/do/catch
You can now just write
API.fetchData(URL)
.map { rawData in
if invalidData(rawData) {
throw myParsingError
}
...
return parsedData
}
- RxCocoa introduces
bindToextensions
combineLatest(firstName.rx_text, lastName.rx_text) { $0 + " " + $1 }
.map { "Greeting \($0)" }
.bindTo(greetingLabel.rx_text)
... works for UITableView/UICollectionView too
viewModel.rows
.bindTo(resultsTableView.rx_itemsWithCellIdentifier("WikipediaSearchCell")) { (_, viewModel, cell: WikipediaSearchCell) in
cell.viewModel = viewModel
}
.addDisposableTo(disposeBag)
- Adds new operators (array version of
zip, array version ofcombineLatest, ...) - Renames
catchtocatchError - Change from
disposeBag.addDisposabletodisposable.addDisposableTo - Deprecates
aggregatein favor ofreduce - Deprecates
variablein favor ofshareReplay(1)(to be consistent with RxJS version)
Check out Migration guide to RxSwift 2.0
Appendix
Using with iOS 7
iOS 7 is installation is little tricky, but it can be done. The main problem is that iOS 7 doesn't support dynamic frameworks.
These are the steps to include RxSwift/RxCocoa projects in an iOS7 project
- RxSwift/RxCocoa projects have no external dependencies so just manually including all of the
.swift,.m,.hfiles in build target should import all of the necessary source code.
You can either do that by copying the files manually or using git submodules.
git submodule add git@github.com:ReactiveX/RxSwift.git
After you've included files from RxSwift and RxCocoa directories, you'll need to remove files that are platform specific.
If you are compiling for iOS, please remove references to OSX specific files located in RxCocoa/OSX.
If you are compiling for OSX, please remove references to iOS specific files located in RxCocoa/iOS.
- Add
RX_NO_MODULEas a custom Swift preprocessor flag
Go to your target's Build Settings > Swift Compiler - Custom Flags and add -D RX_NO_MODULE
- Include
RxCocoa.hin your bridging header
If you already have a bridging header, adding #import "RxCocoa.h" should be sufficient.
If you don't have a bridging header, you can go to your target's Build Settings > Swift Compiler - Code Generation > Objective-C Bridging Header and point it to [path to RxCocoa.h parent directory]/RxCocoa.h.