☕️ Looking for Cocoa extensions?
🎉 Getting Started
ReactiveSwift offers composable, declarative and flexible primitives that are built around the grand concept of streams of values over time. These primitives can be used to uniformly represent common Cocoa and generic programming patterns that are fundamentally an act of observation, e.g.:
- Delegate methods
- Callback blocks
- Notifications
- Control actions and responder chain events
- Futures and promises
- Key-value observing (KVO)
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 the Framework 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.
Please note that the following examples use Cocoa extensions in ReactiveCocoa for illustration.
The first step is to observe edits to the text field, using a RAC extension to
UITextField
specifically for this purpose:
let searchStrings = textField.reactive.continuousTextValues
This gives us a Signal which sends values of type String?
.
With each string, we want to execute a network request. ReactiveSwift offers an
URLSession
extension for doing exactly that:
let searchResults = searchStrings
.flatMap(.latest) { (query: String?) -> SignalProducer<(Data, URLResponse), NSError> in
let request = self.makeSearchRequest(escapedQuery: query)
return URLSession.shared.reactive.data(with: request)
}
.map { (data, response) -> [SearchResult] in
let string = String(data: data, encoding: .utf8)!
return self.searchResults(fromJSONString: string)
}
.observe(on: 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
(using 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!
Since the source of search strings is a Signal
which has a hot signal semantic,
the transformations we applied are automatically evaluated whenever new values are
emitted from searchString
.
Therefore, we can simply observe the signal using Signal.observe(_:)
:
searchResults.observe { event in
switch event {
case let .value(results):
print("Search results: \(results)")
case let .error(error):
print("Search error: \(error)")
case .completed, .interrupted:
break
}
}
Here, we watch for the Value
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<(Data, URLResponse), NSError> in
let request = self.makeSearchRequest(escapedQuery: query)
return URLSession.shared.reactive
.data(with: request)
.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<(Data, URLResponse), NSError> in
let request = self.makeSearchRequest(escapedQuery: query)
return URLSession.shared.reactive
.data(with: request)
.retry(upTo: 2)
.flatMapError { error in
print("Network error occurred: \(error)")
return SignalProducer.empty
}
}
.map { (data, response) -> [SearchResult] in
let string = String(data: data, encoding: .utf8)!
return self.searchResults(fromJSONString: string)
}
.observe(on: 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.reactive.continuousTextValues
.throttle(0.5, on: QueueScheduler.main)
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.reactive.continuousTextValues
.throttle(0.5, on: QueueScheduler.main)
.on(event: { print ($0) }) // the side effect
This will print the stream's events, while preserving the original stream behaviour. Both SignalProducer
and Signal
provide the logEvents
operator, that will do this automatically for you:
let searchString = textField.reactive.continuousTextValues
.throttle(0.5, on: QueueScheduler.main)
.logEvents()
For more information and advance usage, check the Debugging Techniques document.
While ReactiveCocoa was inspired and heavily influenced by ReactiveX (Rx), ReactiveSwift is an opinionated implementation of functional reactive programming, and intentionally not a direct port like RxSwift.
ReactiveSwift differs from ReactiveX in places that:
- Results in a simpler API
- Addresses common sources of confusion
- Matches closely to Swift, and sometimes Cocoa, conventions
The following are a few important differences, along with their rationales.
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.
ReactiveSwift addresses this 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
communicate intent much better.
In other words, ReactiveSwift’s changes here are simple, not easy.
When Signals and SignalProducers are allowed to fail 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.
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
.
ReactiveSwift, on the other hand, focuses on being a native Swift citizen first and foremost, following the Swift API Guidelines as appropriate. Other naming differences are typically inspired by significantly better alternatives from Haskell or Elm (which is the primary source for the “signal” terminology).
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 for Actions.
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 macOS 10.9+, iOS 8.0+, watchOS 2.0+, tvOS 9.0+ and Linux.
If you use [Carthage][] to manage your dependencies, simply add
ReactiveSwift to your Cartfile
:
github "ReactiveCocoa/ReactiveSwift"
If you use Carthage to build your dependencies, make sure you have added ReactiveSwift.framework
, and Result.framework
to the "Linked Frameworks and Libraries" section of your target, and have included them in your Carthage framework copying build phase.
If you use [CocoaPods][] to manage your dependencies, simply add
ReactiveSwift to your Podfile
:
pod 'ReactiveSwift', :git => 'https://github.com/ReactiveCocoa/ReactiveSwift.git'
If you use Swift Package Manager, simply add ReactiveSwift as a dependency
of your package in Package.swift
:
.Package(url: "https://github.com/ReactiveCocoa/ReactiveSwift.git", "1.0.0-alpha.3")
- Add the ReactiveSwift repository as a [submodule][] of your application’s repository.
- Run
git submodule update --init --recursive
from within the ReactiveCocoa folder. - Drag and drop
ReactiveSwift.xcodeproj
andCarthage/Checkouts/Result/Result.xcodeproj
into your application’s Xcode project or workspace. - On the “General” tab of your application target’s settings, add
ReactiveSwift.framework
, andResult.framework
to the “Embedded Binaries” section. - If your application target does not contain Swift code at all, you should also
set the
EMBEDDED_CONTENT_CONTAINS_SWIFT
build setting to “Yes”.
We also provide a great Playground, so you can get used to ReactiveCocoa's operators. In order to start using it:
- Clone the ReactiveSwift repository.
- Retrieve the project dependencies using one of the following terminal commands from the ReactiveSwift project root directory:
git submodule update --init --recursive
OR, if you have Carthage installedcarthage checkout
- Open
ReactiveSwift.xcworkspace
- Build
Result-Mac
scheme - Build
ReactiveSwift-macOS
scheme - Finally open the
ReactiveSwift.playground
- Choose
View > Show Debug Area
If you need any help, please visit our GitHub issues or Stack Overflow. Feel free to file an issue if you do not manage to find any solution from the archives.