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()

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.

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)
}

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 }

producermutiplyByTwo 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:

  1. 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:

  1. 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 or SignalProducerConvertible 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):

  1. Start using functional aspects that Swift gives you for free. Like: mapflatMapfilterreduceor create your own. This will make the usage of high order functions more intuitive. Without this, you will struggle and feel demotivated.
  2. 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.
  3. 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.
  4. You will feel tremendously tempted to use a given result (e.g. [FooBars]) inside the startWithNext closure. Always strive for binding, instead of setting values manually. Binding, not only makes the code more readable, it also makes memory management easier.
  5. 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.
  6. 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.
  7. 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.
  8. FRP can be applied to many different problems.