ReactiveSwift is a Swift framework inspired by Functional Reactive Programming . It provides APIs for composing and transforming streams of values over time .
If you’re already familiar with functional reactive programming or what ReactiveSwift is about, check out theDocumentation folder for more in-depth information about how it all works. Then, dive straight into our documentation comments for learning more about individual APIs.
If you'd like to use ReactiveSwift with Apple's Cocoa frameworks,ReactiveCocoa provides extensions that work with ReactiveSwift.
If you have a question, please see if any discussions in ourGitHub issues or Stack Overflow have already answered it. If not, please feel free tofile your own!
This documents ReactiveSwift 3.x which targets Swift 3.0.x
. For Swift 2.x
support seeReactiveCocoa 4.
ReactiveSwift is inspired by functional reactive programming
. Rather than using mutable variables which are replaced and modified in-place, RAC offers “event streams,” represented by the
Signal
and
SignalProducer
types, that send values over time.
Event streams unify common patterns for asynchrony and event handling, including:
Because all of these different mechanisms can be represented in the same way, it’s easy to declaratively chain and combine them together, with less spaghetti code and state to bridge the gap.
For more information about the concepts in ReactiveSwift, see theFramework Overview.
Let’s say you have a text field, and whenever the user types something into it, you want to make a network request which searches for that query.
The first step is to observe edits to the text field, using a RAC extension to UITextField
specifically for this purpose:
let searchStrings = textField.rac_textSignal() .toSignalProducer() .map { text in text as! String }
This gives us asignal producer which sends values of type String
.
(The cast iscurrently necessary to bridge this extension method from Objective-C.)
With each string, we want to execute a network request. Luckily, RAC offers an NSURLSession
extension for doing exactly that:
let searchResults = searchStrings .flatMap(.Latest) { (query: String) -> SignalProducer<(NSData, NSURLResponse), NSError> in let URLRequest = self.searchRequestWithEscapedQuery(query) return NSURLSession.sharedSession().rac_dataWithRequest(URLRequest) } .map { (data, URLResponse) -> String in let string = String(data: data, encoding: NSUTF8StringEncoding)! return self.parseJSONResultsFromString(string) } .observeOn(UIScheduler())
This has transformed our producer of String
s into a producer of Array
s containing the search results, which will be forwarded on the main thread (thanks to the
UIScheduler
).
Additionally,
flatMap(.Latest)
here ensures that only one search
—the latest—is allowed to be running. If the user types another character while the network request is still in flight, it will be cancelled before starting a new one. Just think of how much code that would take to do by hand!
This won’t actually execute yet, because producers must be started in order to receive the results (which prevents doing work when the results are never used). That’s easy enough:
searchResults.startWithNext { results in print("Search results: /(results)") }
Here, we watch for the Next
event, which contains our results, and just log them to the console. This could easily do something else instead, like update a table view or a label on screen.
In this example so far, any network error will generate a Failed
event, which will terminate the event stream. Unfortunately, this means that future queries won’t even be attempted.
To remedy this, we need to decide what to do with failures that occur. The quickest solution would be to log them, then ignore them:
.flatMap(.Latest) { (query: String) -> SignalProducer<(NSData, NSURLResponse), NSError> in let URLRequest = self.searchRequestWithEscapedQuery(query) return NSURLSession.sharedSession() .rac_dataWithRequest(URLRequest) .flatMapError { error in print("Network error occurred: /(error)") return SignalProducer.empty } }
By replacing failures with the empty
event stream, we’re able to effectively ignore them.
However, it’s probably more appropriate to retry at least a couple of times before giving up. Conveniently, there’s a
retry
operator to do exactly that!
Our improved searchResults
producer might look like this:
let searchResults = searchStrings .flatMap(.Latest) { (query: String) -> SignalProducer<(NSData, NSURLResponse), NSError> in let URLRequest = self.searchRequestWithEscapedQuery(query) return NSURLSession.sharedSession() .rac_dataWithRequest(URLRequest) .retry(2) .flatMapError { error in print("Network error occurred: /(error)") return SignalProducer.empty } } .map { (data, URLResponse) -> String in let string = String(data: data, encoding: NSUTF8StringEncoding)! return self.parseJSONResultsFromString(string) } .observeOn(UIScheduler())
Now, let’s say you only want to actually perform the search periodically, to minimize traffic.
ReactiveCocoa has a declarative throttle
operator that we can apply to our search strings:
let searchStrings = textField.rac_textSignal() .toSignalProducer() .map { text in text as! String } .throttle(0.5, onScheduler: QueueScheduler.mainQueueScheduler)
This prevents values from being sent less than 0.5 seconds apart.
To do this manually would require significant state, and end up much harder to read! With ReactiveCocoa, we can use just one operator to incorporate time into our event stream.
Due to its nature, a stream's stack trace might have dozens of frames, which, more often than not, can make debugging a very frustrating activity. A naive way of debugging, is by injecting side effects into the stream, like so:
let searchString = textField.rac_textSignal() .toSignalProducer() .map { text in text as! String } .throttle(0.5, onScheduler: QueueScheduler.mainQueueScheduler) .on(event: { print ($0) }) // the side effect
This will print the stream'sevents, while preserving the original stream behaviour. Both
SignalProducer
and
Signal
provide the logEvents
operator, that will do this automatically for you:
let searchString = textField.rac_textSignal() .toSignalProducer() .map { text in text as! String } .throttle(0.5, onScheduler: QueueScheduler.mainQueueScheduler) .logEvents()
For more information and advance usage, check theDebugging Techniques document.
ReactiveCocoa was originally inspired, and therefore heavily influenced, by Microsoft’s Reactive Extensions (Rx) library. There are many ports of Rx, includingRxSwift, but ReactiveCocoa is intentionally not a direct port.
Where ReactiveSwift differs from Rx, it is usually to:
The following are some of the concrete differences, along with their rationales.
In most versions of Rx, Streams over time are known as Observable
s, which parallels the Enumerable
type in .NET. Additionally, most operations in Rx.NET borrow names from LINQ
, which uses terms reminiscent of relational databases, like Select
and Where
.
RAC is focused on matching Swift naming first and foremost, with terms like map
and filter
instead. Other naming differences are typically inspired by significantly better alternatives from Haskell
or Elm
(which is the primary source for the “signal” terminology).
One of the most confusing aspects of Rx is that of “hot”, “cold”, and “warm” observables (event streams).
In short, given just a method or function declaration like this, in C#:
IObservable<string> Search(string query)
… it is impossible to tell
whether subscribing to (observing) that IObservable
will involve side effects. If it does
involve side effects, it’s also impossible to tell whether each subscription
has a side effect, or if only the first one does.
This example is contrived, but it demonstrates a real, pervasive problem that makes it extremely hard to understand Rx code (and pre-3.0 ReactiveCocoa code) at a glance.
[ReactiveCocoa 3.0][ https://github.com/ReactiveCocoa/ReactiveCocoa/blob/master/CHANGELOG.md
] has solved this problem by distinguishing side effects with the separate
Signal
and
SignalProducer
types. Although this means there’s another type to learn about, it improves code clarity and helps communicates intent much better.
In other words, ReactiveSwift’s changes here are simple, not easy .
Whensignals andsignal producers are allowed tofail in ReactiveSwift, the kind of error must be specified in the type system. For example, Signal<Int, NSError>
is a signal of integer values that may fail with an error of type NSError
.
More importantly, RAC allows the special type NoError
to be used instead, which statically guarantees
that an event stream is not allowed to send a failure. This eliminates many bugs caused by unexpected failure events.
In Rx systems with types, event streams only specify the type of their values—not the type of their errors—so this sort of guarantee is impossible.
Rx is basically agnostic as to how it’s used. Although UI programming with Rx is very common, it has few features tailored to that particular case.
ReactiveSwift takes a lot of inspiration from ReactiveUI , including the basis forActions.
Unlike ReactiveUI, which unfortunately cannot directly change Rx to make it more friendly for UI programming, ReactiveSwift has been improved many times specifically for this purpose —even when it means diverging further from Rx.
ReactiveSwift supports OS X 10.9+
, iOS 8.0+
, watchOS 2.0
, and tvOS 9.0
.
To add RAC to your application:
git submodule update --init --recursive
from within the ReactiveSwift folder. ReactiveSwift.xcodeproj
and Carthage/Checkouts/Result/Result.xcodeproj
into your application’s Xcode project or workspace. ReactiveSwift.framework
and Result.framework
to the “Embedded Binaries” section. EMBEDDED_CONTENT_CONTAINS_SWIFT
build setting to “Yes”.
Or, if you’re usingCarthage, simply add ReactiveSwift to your Cartfile
:
github "ReactiveCocoa/ReactiveSwift"
Make sure to add both ReactiveSwift.framework
and Result.framework
to "Linked Frameworks and Libraries" and "copy-frameworks" Build Phases.
Once you’ve set up your project, check out theFramework Overview for a tour of ReactiveSwift’s concepts, and theBasic Operators for some introductory examples of using it.
We also provide a great Playground, so you can get used to ReactiveCocoa's operators. In order to start using it:
git submodule update --init --recursive
OR
, if you haveCarthage installed carthage checkout
ReactiveSwift.xcworkspace
Result-Mac
scheme ReactiveSwift-macOS
scheme ReactiveSwift.playground
View > Show Debug Area