A half-hour to learn ReactiveSwift
*Note: Inspired by A half-hour to learn Rust by fasterthanlime. ❤️*
ReactiveSwift, and other FRP libraries, can take months to learn. In this post, we will cover the most important entities, operators and techniques. By the end, you should feel comfortable using, reading and understanding code that uses ReactiveSwift.
If you know Learn X in Y minutes, you will be right at home.
Let’s do this!
Signal<T, E: Error>
is the Observer pattern in disguise:
let (signal, observer) = Signal<Int, Never>.pipe()
signal.observeValues { value in
// this will be called when the observer sends `1`
print(value)
}
observer.send(value: 1)
Signal<T, E: Error>
can send events indefinitely, unless you complete it:
let (signal, observer) = Signal<Int, Never>.pipe()
signal.observeValues { value in
print(value)
}
observer.sendCompleted()
// `2` will never be printed
observer.send(value: 2)
… or it errors out:
let (signal, observer) = Signal<Int, MyError>.pipe()
signal.observeValues { value in
print(value)
}
observer.send(error: anError)
// `2` will never be printed
observer.send(value: 2)
After a completion, error or interruption the signal terminates.
Rarely we only care about the error:
signal.observeFailed { error in
print(error)
}
observer.send(error: anError)
More often we care about the completion:
signal.observeCompleted {
print("Completed!")
}
observer.sendCompleted()
A Signal<T, E: Error>
that can fail doesn’t expose observeValues
. We have to use observeResult
:
signal.observeResults { result
switch result {
case let .success(value):
break
case let .failure(error):
break
}
}
observer.sendCompleted()
This is a deliberate design choice. I call it good taste.
A Signal<T, E: Error>
provides many of the operations that we are used to in Swift:
signal.map { $0 + 2 } // 2 + 2 == 4
observer.send(value: 2)
signal.filter { $0 > 0 } // -1 > 0 == false
observer.send(value: -1)
signal.reduce(1, +)
observer.send(value: 2) // 1 + 2 = 3
observer.send(value: 3) // 3 + 3 = 6
Because Signal<T, E: Error>
operates on events over time, it has access to operations that neither Optional<T>
or Result<T, E: Error>
have:
signal.skip(first: 2)
observer.send(value: 5) // First value is skipped ❌
observer.send(value: 8) // Second value is skipped ❌
observer.send(value: 13) // Third value goes through ✅
signal.take(first: 2)
observer.send(value: 5) // First value goes through ✅
observer.send(value: 8) // Second value goes throughd ✅
observer.send(value: 13) // Third value is discarded ❌
A lot of times, we want to discard repeated values:
signal.skipRepeats()
observer.send(value: 1) // This value goes through ✅
observer.send(value: 1) // This value is discarded ❌
observer.send(value: 2) // This value goes through ✅
observer.send(value: 1) // This value goes through ✅
Other times, far less often, we want all values to be unique:
signal.uniqueValues()
observer.send(value: 1) // This value goes through ✅
observer.send(value: 1) // This value is discarded ❌
observer.send(value: 2) // This value goes through ✅
observer.send(value: 1) // This value is discarded ❌
We can also control the events flow:
// Think of a Scheduler as a DispatchQueue. The code reads as:
// I will do this work, on this DispatchQueue.
signal.throttle(1, QueueScheduler())
// Time: 0s
observer.send(value: 1)
// Time: 1s
// Value `1` goes through ✅
// Time: 1.5s
observer.send(value: 2)
observer.send(value: 3)
observer.send(value: 4)
// Time: 2s
// Value `4` goes through ✅
// Time: 2.5s
observer.send(value: 5)
// Time: 3s
// Value `5` goes through ✅
If we have a button that on tap makes a request, throttle
can be useful to avoid the user spamming it.
signal.debounce(1, QueueScheduler())
// Time: 0s
observer.send(value: 1)
// Time: 1s
// Value `1` goes through ✅
// Time: 1.5s
observer.send(value: 2)
observer.send(value: 3)
// Time: 2.0s
observer.send(value: 4)
// Time: 3s
// Value `4` goes through ✅
debounce
is useful when we want to give some leeway to the user’s actions: typing on a search bar and fetching results. As the user types, we want to wait 1 second, when they stop, before making the request.
It’s possible to delay the event:
signal.delay(3, QueueScheduler()).observeValues { value in
print(value)
}
// Time: 0s
observer.send(value: 1) // Nothing happens
// Time: 3
// "1" is printed
We rarely do this, since it is equivalent to DispatchQueue().asyncAfter(deadline:execute:)
. It’s a code smell. Use it for debugging purposes.
We can merge signals:
Signal.merge(signal1, signal2).observeValues { value in
print(value)
}
// we can also do `signal1.merge(signal2)`
signal1.send(value: 1) // prints "1"
signal2.send(value: 2) // prints "2"
signal1.send(value: 3) // prints "3"
We can combine the signals events:
Signal.combineLatest(signal1, signal2).observeValues { value in
print(value)
}
// we can also do `signal1.combineLatest(signal2)`
signal1.send(value: 1) // Nothing happens
signal2.send(value: 2) // prints "(1, 2)"
signal1.send(value: 3) // prints "(3, 2)"
Sometimes we want both signals to send the same number of events in order to do something with them. This is unusual and we don’t use it as much:
Signal.zip(signal1, signal2).observeValues { value in
print(value)
}
// we can also do `signal1.zip(signal2)`
signal1.send(value: 1) // Nothing happens
signal2.send(value: 2) // prints "(1, 2)"
signal1.send(value: 3) // Nothing happens
signal2.send(value: 4) // prints "(3, 4)"
It can be useful to combine the newest value with the previous:
signal.combinePrevious().observeValues { value in
print(value)
}
signal.send(value: 1) // Nothing happens
signal.send(value: 2) // prints "(1, 2)"
signal.send(value: 3) // prints "(2, 3)"
signal.send(value: 4) // prints "(3, 4)"
We can also use withLatest
, although less often:
signal1.withLatest(signal2).observeValues { value in
print(value)
}
signal2.send(value: 1) // Nothing happens
signal2.send(value: 2) // Nothing happens
signal1.send(value: 3) // prints "(3, 2)"
If we want to observe values on the main queue we can:
signal.observe(on: QueueScheduler.main).observeValues { value in
print("Print value in the main queue")
}
We can also:
signal.observe(on: UIScheduler()).observeValues { value in
print("Print value in the main queue")
}
The former schedules the work asynchronously, while the latter will do it synchronously, if it’s already running in the main queue. It’s confusing. I know. Most of the time, it doesn’t matter.
Signal<T, E: Error>
s are one way to bridge delegates with the reactive world:
class LocationManager: NSObject, CLLocationManagerDelegate {
let locations: Signal<[CLLocation], Never>
private let observer: Signal<[CLLocation], Never>.Observer
override init() {
(locations, observer) = Signal<[CLLocation], Never>.pipe()
super.init()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
observer.send(value: locations)
}
}
This allows us to interact with Apple’s frameworks that rely on the delegation pattern.
SignalProducer<T, E: Error>
is the other side of the coin. Usually we interact more with this entity than with Signal<T, E: Error>
.
We can create a producer like this:
let producer = SignalProducer(value: 5)
This is equivalent:
let producer = SignalProducer { observer, disposable in
observer.send(value: 5)
observer.sendCompleted()
}
We can map
values:
let mutiplyByTwo = producer.map { $0 * 2}
The transformation ( $0 * 2
) hasn’t happened yet. We need to start:
mutiplyByTwo.startWithValues { value in
print(value) // "10"
}
The concept of starting a SignalProducer
is important, always keep it in mind.
You can also decide where the work is done and where the value is observed:
producer
.start(on: QueueScheduler())
.observe(on: QueueScheduler.main)
.startWithValues { value in
print(value)
}
A SignalProducer<T, E: Error>
is a value type, so this means that each variable holds its own copy:
let mutiplyByTwo = producer.map { $0 * 2 }
let mutiplyByFour = mutiplyByTwo.map { $0 * 2 }
producer
, mutiplyByTwo
and mutiplyByFour
are independent of each other the same way as:
let x = 5
let y = x * 2
let z = y * 2
In contrast, Signal<T, E: Error>
is a reference type, and the above example is not applicable. The hint is given on the method names: startWithValues
versus observeValues
.
The vast majority of operations on Signal<T, E: Error>
are available to SignalProducer<T, E: Error>
and vice-versa.
Both entities can be mixed together. This is possible via the common protocol that they both comply with: SignalProducerConvertible
. Most of the time you don’t have to think about it.
Use a Signal<T, E: Error>
when:
- It’s something that doesn’t necessarily ends: tapping on a button, receiving notifications, the user’s location changing.
Use a SignalProducer<T, E: Error>
when:
- It’s something that has a beginning and an end: a network request, parsing JSON, persisting data on a database.
flatMap
flatMap
is our workhorse and the most important operator you can learn.
Beginners struggle with the difference between map
and flatMap
. Ask yourself: can this transformation fail? If yes, use the latter, if not use the former. There are exceptions to this rule.
There are 5 strategies when using flatMap
.
1. concurrent(limit: UInt)
2. latest
3. race
4. concat == concurrent(limit: 1)
5. merge == concurrent(limit: UInt.max)
The one you will use the most is latest
. The second is likely merge
and finally concat
.
// Keep a strong reference to `tap`, otherwise it will be deallocated
// when the function scope is destroyed
let (tap, observer) = Signal<Void, Never>.pipe()
// It will take 5 seconds, for this producer to send "1".
let longRunningTask = SignalProducer(value: 1).delay(5, on: QueueScheduler())
tap.flatMap(.latest) {
longRunningTask
}.observeValues { (value: Int) in
print(value)
}
// Time: 0s
observer.send(value: ()) // `longRunningTask` task starts
// Time: 1s
// Time: 2s
observer.send(value: ()) // first `longRunningTask` is cancelled. a second `longRunningTask` starts
// Time: 3s
// ...
// Time: 7s
// "1" is printed
As new events arrive on tap.flatMap(.latest)
, any on going work is cancelled and the newest starts. .latest
is used when there’s a a 1-to-1 relationship between an action (e.g. user tap) and work to-be-done (e.g. network request). Tapping multiple times on a button should yield a single result, not many.
tap.flatMap(.merge) {
longRunningTask
}.observeValues { (value: Int) in
print(value)
}
// Time: 0s
observer.send(value: ()) // `longRunningTask` task starts
// Time: 1s
// Time: 2s
observer.send(value: ()) // another `longRunningTask` task starts
// Time: 5s
// "1" is printed
// Time: 7s
// "1" is printed
.merge
is used mostly when interacting with UI. The order doesn’t matter, as long as the values arrive, as you saw previously with Signal.merge
.
.concat
cares about the order and it’s not often used with UI:
tap.flatMap(.merge) {
longRunningTask
}.observeValues { (value: Int) in
print(value)
}
// Time: 0s
observer.send(value: ()) // `longRunningTask` task starts
// Time: 1s
// Time: 2s
observer.send(value: ()) // another `longRunningTask` task starts
// Time: 5s
// "1" is printed
// Time: 10s
// "1" is printed
The second longRunningTask
starts only when the first one completes.
.race
is an oddball. Due to its indeterministic nature, it’s rarely used.
Notice that the longRunningTask
is started, even tho we didn’t explicitly called start
.
While flatMap
gives you the previous yielded value, sometimes we don’t really care about it:
let producer = longRunningTask.then(anotherLongRunningTask)
anotherLongRunningTask
starts when longRunningTask
completes. then
is used more than you would think.
Alongside flatMap
, we have flatMapError
. It allows us to recover from errors:
let failure = SignalProducer(error: myError)
let producer = failure.flatMapError { (error: MyError) in
SignalProducer(value: 1)
}
producer.startWithValues { value in
print(value) // "1"
}
SignalProducer<T, E: Error>
are also used to bridge Apple’s code with the reactive world:
SignalProducer<Data, Error> { observer, disposable in
let task = URLSession.shared.dataTask(with: anURL) { (data, response, error) in
if let anError = error {
observer.send(error: anError)
}
if let data = data {
observer.send(value: data)
observer.sendCompleted()
}
}
disposable.observeEnded { task.cancel() }
}
Rule of the thumb: delegates can be covered by Signals. Blocks/Closures based APIs can be covered by SignalProducers.
Memory Management
ReactiveSwift favours a more declarative approach to memory management:
producer.take(first: 1).startWithValues { value in
}
The producer will clean up itself, after the first element arrives.
You can also:
producer.take(duringLifetimeOf: self).startWithValues { value in
}
Once self
is deinitialized, the producer is cleaned.
The take
’s family of operators provide many ways of managing the information flow and, as an extra, the lifetime of the producer (or signal).
Sometimes we need to be explicit about memory management, for that we use a Disposable
.
let disposable = producer.startWithValue { value in }
disposable.dispose()
We make the disposable a property of the entity:
class ViewController: UIViewController {
private var disposable: Disposable?
deinit {
// This is not necessary
self.disposable?.dispose()
}
override func viewDidLoad() {
super.viewDidLoad()
self.disposable = producer.startWithValue { value in
}
}
}
When the ViewController
gets deallocated, the disposable
is deallocated as well and it cleans the subscription created by the producer.startWithValue
.
In some cases there are too many disposables around. For that we use a CompositeDisposable
instead:
compositeDisposable += producer.startWithValue { value in }
compositeDisposable += producer1.startWithValue { value in }
compositeDisposable.dispose()
We use the operator +=
to make this chore less of a chore.
Most of the times tho, we get memory leaks because we reference self
inside the closure:
producer.startWithValue { value in
self.updateUI(with: value)
}
Make those self
weak:
producer.startWithValue {[weak self] value in
self?.updateUI(with: value)
}
Property
By now you might have noticed that there’s no concept of a “current” value. Events arrived, we do something with them, and then they are gone. A Property<T>
is used when we need the “now”:
let property = Property(value: 2)
This is not particularly exciting. We still have access to some operations, like map
:
let property = Property(value: 2).map { $0 * 2 }
We can now extract the value if we wish to:
let value = property.value // 4
Notice that this is not possible to do on a SignalProducer<T, E: Error>
or a Signal<T, E: Error>
.
We can also extract a producer
, or a signal
, out of a property:
let producer: SignalProducer<Int, Never> = property.producer
let signal: Signal<T, Never> = property.signal
Notice the following tho:
let producer: SignalProducer<Int, Never> = property.producer
producer.startWithValues { value in
print(value)
}
// "4" is printed
versus a signal:
let signal: SignalProducer<Int, Never> = property.signal
signal.observeValues { value in
print(value)
}
// Nothing is printed
This behaviour is expected. By the time we start observing values, the value is already “gone”. The lesson is: when starting observing a signal, only new events are seen.
Properties become exciting when there is something mutating them:
let timer: SignalProducer<Int, Never> = SignalProducer.timer(interval: .second(1), on: QueueScheduler())
.map { _ in 1 }
.combineWithPrevious()
.map(+)
let property = Property(initial: 1, then: timer)
The timer
sends a value (a Date
) every 1 second. We are not particularly interested in dates. So we discard the value and replace it with a 1
. We then combine that value with the previous one. Finally we sum them up.
Suffice to say we get something like this:
Time 0: 1
Time 1: 1 + 1
Time 2: 2 + 1
Time 3: 3 + 1
The property
is being updated every second, which is pretty cool.
Property<T>
has a counterpart: MutableProperty<T>
. They are pretty much the same, except you can change the value directly:
let mutableProperty: MutableProperty<Int> = MutableProperty(value: 1)
mutableProperty.value = 2
I personally prefer using functions to modify a Property
:
class MyViewModel {
let property: Property<Int>
let (signal, observer) = Signal<Int, Never>.pipe()
init(initial: Int) {
self.property = Property(initial: initial, then: signal)
}
func mutate(value: Int) {
observer.send(value: value)
}
}
This is equivalent to:
class MyViewModel {
let property: MutableProperty<Int>
init(initial: Int) {
self.value = MutableProperty(value: initial)
}
}
Less code is usually better. The problem is that it might be difficult to track what’s accessing the property’s value
.
Of course, use whatever feels right to you.
In a perfect world, one wouldn’t need to use properties at all. We would receive events, transform them, and display them on the UI. A lot of times, this is possible and one should strive for that.
Property<T>
and MutableProperty<T>
can be interchangeable when functions accept a PropertyProtocol
. Most of the times, you don’t have to think about it, much like SignalProducerConvertible
.
Action
As discussed, a SignalProducer<T, E: Error>
is a value type. When this happens:
producer.startWithValues { value in
print(value)
}
No one else has access to those value
outside the closure. Sometimes we want to. For that we use an Action<Input, Output, E: Error>
.
let action: Action<Void, Int, Never> = Action {
SignalProducer(value: 5)
}
let producer: SignalProducer<Int, Never> = action.apply()
Notice that when the apply()
method is called, a producer
is returned. And we can use everything we learned so far.
The Input
parameter works as you would expect:
let action: Action<Int, Int, Never> = Action { input in
SignalProducer(value: input)
}
let producer: SignalProducer<Int, Never> = action.apply(4)
producer.startWithValues { value in
print(value) // it's a 4!
}
Action<Input, Output, E: Error>
provides a lot of ways of being initialised.
One of my favourites is enabled/disabled based on a Property<Bool>
:
let property = Property(value: false)
let action: Action<Int, Int, Never> = Action(enabledIf: property, execute: { value in
SignalProducer(value: input)
})
let producer = action.apply(4)
producer.startWithValues { value in
print(value) // Nothing happens
}
An Action<Input, Output, E: Error>
packs a lot of functionality. For instance, we can observe the values coming:
action.values.observeValues { values in
print(values)
}
Or its completion:
action.values.observeCompleted {
print("Completed")
}
We can also show a spinner and track its progress it via:
let isExecuting: Property<Bool> = action.isExecuting
A good candidate to use an Action<Input, Output, E: Error>
is when the user taps a button and a request is made. From the View perspective, we might want to enable/disable other parts of the screen, while this is happening.
As you can see an Action<Input, Output, E: Error>
is a very versatile entity.
More experience ReactiveSwift users will tell you that you don’t actually need to use an Action<Input, Output, E: Error>
at all. And this is quite true. Nevertheless, as a newcomer, much like MutableProperty<T>
, using an Action<Input, Output, E: Error>
is a perfectly valid way of structuring your code.
We have now covered the most important entities that ReactiveSwift provides.
Let’s move on to ReactiveCocoa.
Ready? Go!
ReactiveCocoa
ReactiveCocoa provides reactive bindings to Apple’s frameworks. Most of the time we only care about two: Foundation and UIKit.
On Foundation we are mainly interested on making internet requests:
let request: URLRequest = ...
let session = URLSession.shared
let producer: SignalProducer<(Data, URLResponse), Error> = session.reactive.data(with: request)
Notice that to access the reactive bindings, these are name-spaced via .reactive
. Some people say this is a trait of a well designed framework. I call it good taste.
We would do with it what we already know:
producer.startWithValues { data, response in
//
}
But we are good citizens, so we deal with the failures:
producer.startWithResult { result in
switch result {
case let .success(data, response):
break
case let .failure(error):
break
}
}
Moving to UIKit:
let textField = TextField()
textField.reactive.continuousTextValues.observeValues { value
print(value)
}
Notice that we can’t “start” observing a textField
, we just observe it.
This is a big source of confusion for beginners and it takes a while to understand when to use a Signal<T, E: Error
(with observeValues
) and a SignalProducer<T, E: Error>
(with startWithValues
).
Notice again the .reactive
. This is important, because we use this a lot to see what functionality the types expose. Next time you feel lost, type .reactive.
+ esc
.
UIButton
taps can be handled two ways:
button.reactive
.controlEvents(.touchUpInside)
.observeValues { button in
/// Do something
}
Or we can use a CocoaAction<Sender>
. A CocoaAction<Sender>
is a wrapper around an Action<Input, Output, E: Error
. Notice the Void
as Input
:
let action: Action<Void, Int, Never> = Action {
SignalProducer(value: 5)
}
button.reactive.pressed = CocoaAction(action)
We can also make it take an input:
let action: Action<Int, Int, Never> = Action { input in
SignalProducer(value: input)
}
button.reactive.pressed = CocoaAction(action, input: 5)
Let’s take it up a notch:
let mutableProperty = MutableProperty("")
let textField = UITextField()
mutableProperty <~ textField.reactive.continuousTextValues
This is equivalent to:
let mutableProperty = MutableProperty("")
let textField = UITextField()
textField.reactive.continuousTextValues.observeValues { value in
mutableProperty.value = value
}
The former is idiomatic ReactiveSwift code. The latter not so much.
We would use this alongside the MVVM Pattern:
class ViewController: UIViewController {
private let textField: UITextField()
private let viewModel: ViewModel
init(viewModel: ViewModel) {
self.viewModel = viewModel
}
override func viewDidLoad() {
super.viewDidLoad()
viewModel.text <~ textField.reactive.continuousTextValues
}
}
The binding operator <~
creates a bound between a source and a target.
These can be sources:
SignalProducer<T, E: Error>
Signal<T, E: Error>
Property<T>
MutableProperty<T>
- Anything complying with
BindingSource
orSignalProducerConvertible where Error == Never
These can be targets:
MutableProperty<T>
BindingTarget<Value>
- Anything complying with
BindingTargetProvider
We can create our own BindingTarget<Value>
, although it’s uncommon to do so.
The binding ( <~
) returns a disposable, so we can:
disposable = viewModel.text <~ textField.reactive.continuousTextValues
General tips
Sometimes we want to inject side effects. Side effects are actions that are insert in the middle of the flow, but don’t interfere with the flow’s data:
let signal: Signal<Int, Never> = tap.flatMap(.merge) {
longRunningTask
}.on(value: { value in
print(value)
})
// versus
let disposable: Disposable = tap.flatMap(.merge) {
longRunningTask
}.observeValues { value in
print(value)
}
on(value:
and observeValues(value:
look similar but they return different types.
Most often, we either use on
or logEvents
for debugging purposes. Any other reason, is likely a code smell and a better way exists.
We often forget to deliver events on the main thread, this causes problems and hopefully the app just crashes and we can fix the issue:
signal
.observe(on: QueueScheduler.main)
.observeValues {[weak self] value in
self?.label.text = value
}
And:
producer
.observe(on: QueueScheduler.main)
.startWithValues {[weak self] value in
self?.label.text = value
}
One of the biggest hurdles with ReactiveSwift versus other FRP frameworks is the typed error. As you have noticed, the unhappy path, is as important, as the successful one.
Because of that, we sometimes have issues making entities with different error types play alongside.
enum AppError: Error {
case network(Error)
case badResponse
}
session.reactive.data(with: request)
.mapError(AppError.network)
.flatMap(.latest) { (data, response) -> SignalProducer<String, AppError> in
guard let httpResponse = response as? HTTPURLResponse else {
return SignalProducer(error: .badResponse)
}
return parse(data)
}
Notice that session.reactive.data(with:
returns its own Error
. But on the flatMap
we want to bridge that Error
with our own error, in this case AppError
. For that we can use mapError
.
That particular line is equivalent to:
.mapError { AppError.network($0 }
The same situations happens when the upstream’s error is Never
. We can promote the producer to a particular error:
let producer: SignalProducer<Int, Never> = SignalProducer(value: 5)
let producerWithError: SignalProducer<Int, AppError> = producer.promoteError(AppError.self)
This doesn’t make the producer
, that would never fail, make it fail now. It simply makes it play alongside with other entities that use AppError
as the parameterised E
.
Sometimes the handler grows quite dramatically. Good citizens split this into functions:
session.reactive.data(with: request)
.mapError(AppError.network)
.flatMap(.latest) { (data, response) -> SignalProducer<String, AppError> in
guard let httpResponse = response as? HTTPURLResponse else {
return SignalProducer(error: .badResponse)
}
return parse(data)
}
Should be translated to:
let toString: (Data, URLResponse) -> SignalProducer<String, AppError> = { data, response in
guard let httpResponse = response as? HTTPURLResponse else {
return SignalProducer(error: .badResponse)
}
return parse(data)
}
session.reactive.data(with: request)
.mapError(AppError.network)
.flatMap(.latest, transformation: toString)
This usually helps the compiler as well.
ReactiveSwift is specially good at Hole Driven Development.
Say for example we have no idea how the toString
function should look like. No problem:
let toString: (Data, URLResponse) -> SignalProducer<String, AppError> = { data, response in
SignalProducer(value: "Hello World")
}
Returning a String
is trivial, but sometimes the object can be quite complex:
let toString: (Data, URLResponse) -> SignalProducer<ComplexEntity, AppError> = { data, response in
.empty
}
.empty
is equivalent to:
SignalProducer { lifetime, observer in
observer.sendCompleted()
}
This is better than a fatalError
since we can actually continue working without crashing the app.
The rest of this article, are the most important points from my previous article (Younger Self):
- Start using functional aspects that Swift gives you for free. Like:
map
,flatMap
,filter
,reduce
, or create your own. This will make the usage of high order functions more intuitive. Without this, you will struggle and feel demotivated. - Initially you will feel that most of the operators don’t fit your needs, but as you learn more, you will see that in fact they do. Rarely I had the need to create new operators. My recommendation is to make an extra effort and exhaust your options, before creating a new one. My DMs are open, if you have any question.
- 99% of the time you won’t care about hot and cold signals. Until you do and lose hours of work. Take a bit of time to understand the difference.
- You will feel tremendously tempted to use a given result (e.g.
[FooBars]
) inside thestartWithNext
closure. Always strive for binding, instead of setting values manually. Binding, not only makes the code more readable, it also makes memory management easier. - Because you don’t know how to manipulate streams of events, you will feel compelled to create flags (boolean properties) inside your class. Nothing is worst than FRP mixed with highly stateful code. A solution in this case, is to ask around in the ReactiveCocoa’s Slack channel. The next step is to learn from the best resources.
- You app architecture will dramatically change when you start seeing it as small blocks of work. Your network layer, won’t know what parsing is, or how you persist your model objects. This will be true for most of your app. A lot of small pieces abstracted via protocols. This will make your code highly reusable and easily testable.
- Because you now know about composition and how easy it is to pass values between different blocks of work via
flatMap
, certain things like logging and analytics, won’t clutter the core of your app. You can just plug-in whatever you want easily. - FRP can be applied to many different problems.