Skip to content

Learnings from RxSwift

More recently, I decided to dig deeper into reactive programming, looking into more resources/blogs about RxSwift.

Leo Kwan
Leo Kwan
5 min read
Learnings from RxSwift
Photo by Andy Holmes / Unsplash

Table of Contents

Introduction to Reactive Programming

When I began programming two years ago, the term "Reactive Cocoa" truly baffled me. Encountering its syntax for the first time was daunting, leaving me wondering, "What on earth is RAC? This was never covered at the Flatiron School. Better to avoid it for now."

More recently, I decided to dig deeper into reactive programming, looking into more resources/blogs about RxSwift. You also know RxSwift has hit the mainstream of iOS swift programming recently after a dedicated book about its gets published on raywenderlich.com.

But before trying to understand a full-fledged framework like RxSwift, I tried becoming more reactive with my code without a framework, and I enjoyed it. Let me explain.

Diving Into Reactive Programming Without a Framework

I started by adopting a simple Swift Observable pattern from a well-known example project called SwiftWeather. This experience deepened my appreciation for reactive programming, especially when incorporating some MVVM principles. The benefits were clear and immediate: I reduced the amount of boilerplate code needed for delegates and protocols, eliminated the need for NotificationCenter (which I rarely used anyway), and significantly lessened the mental effort required to track the various states of my objects, particularly within view controllers.

class Observable1<T> {
   
  typealias Observer = (T) -> Void
  private(set) var observer: Observer?
   
  func observe(_ observer: Observer?) {
    self.observer = observer
  }
   
  var value: T {
    didSet {
      observer?(value)
    }
  }
   
  init(_ v: T) {
    value = v
  }
}

I’d simply enclose a value in this class named ‘Observable’, and this object would take in one observer that subscribed to the wrapped value’s changes.

The core line lies in the ‘value’ didSet method. Every time a value is set, the observer, which is a function that accepts the wrapped value’s type, gets called with the updated value.

1
2
3
4
5
var value: T {
    didSet {
      observer?(value)
    }
}

There’s really no magic to it; as another developer who realized this Observable/KVO pattern pointed out, It would follow all the existing rules of memory management and variable scope as any other closure

class ViewController: UIViewController {
   
  var currentWeather: Observable<Double> = Observable(50)
   
  override func viewDidLoad() {
    super.viewDidLoad()
    
    currentWeather.observe { (newPrice) in
      print("1. Updated Temperature: \(newPrice)")
    }
     
    currentWeather.value = 50 // 1. Updated Temperature: 50
    currentWeather.value = 51 // 1. Updated Temperature: 51
    currentWeather.value = 52 // 1. Updated Temperature: 52    
  }
}

Simple example, not realistic. In reality, I probably want multiple things subscribing to currentWeather’s changes.

class ViewController: UIViewController {
   
  var currentWeather: Observable1<Int> = Observable1(50)
   
  override func viewDidLoad() {
    super.viewDidLoad()
     
    currentWeather.observe { (newPrice) in
      print("1. Updated Temperature: \(newPrice)")
    }
     
    // Added a second observer
    currentWeather.observe { (newPrice) in
      print("2. Updated Temperature: \(newPrice)")
    }
     
    // Added a third observer
    currentWeather.observe { (newPrice) in
      print("3. Updated Temperature: \(newPrice)")
    }
     
    currentWeather.value = 50 // 3. Updated Temperature: 50
    currentWeather.value = 51 // 3. Updated Temperature: 51
    currentWeather.value = 52 // 3. Updated Temperature: 52    
     
  }
}

Looks go– Wait. That’s not right. I get print statements from only the third observer — what about the 1st and 2nd ones?

Looking back at Observable, I see that observer is a single property; so every time subscribe get called on currentWeather, the last observer gets written over by the newly passed in one.

Implementing the Observable Pattern

This is when things got difficult. On first though, the most practical thing to do in order for Observable to push changes to multiple observers is to change observer: Observer<T> to an observers: [Observer<T>]. Sounds like a quick and easy fix!

class Observable<T> {
   
  typealias Observer = (T) -> Void
  private(set) var observers: [Observer] = [] // change to an array
 
  func observe(_ observer: @escaping Observer) {
    // append observer to array
    observers.append(observer)
  }
   
  var value: T {
    didSet {
      // iterate over observers and call each closure newly set value
      observers.forEach({$0(value)})
    }
  }
   
  init(_ v: T) {
    value = v
  }
}


There. Now we should be able to notify every function/observer in our observers array.

currentWeather.observe { (newPrice) in
      print("1. Updated Temperature: \(newPrice)")
}
     
currentWeather.observe { (newPrice) in
      print("2. Updated Temperature: \(newPrice)")
}
     
currentWeather.observe { (newPrice) in
      print("3. Updated Temperature: \(newPrice)")
}
     
currentWeather.value = 51
currentWeather.value = 52
currentWeather.value = 53
// 1. Updated Temperature: 51
// 2. Updated Temperature: 51
// 3. Updated Temperature: 51
// 1. Updated Temperature: 52
// 2. Updated Temperature: 52
// 3. Updated Temperature: 52
// 1. Updated Temperature: 53
// 2. Updated Temperature: 53
// 3. Updated Temperature: 53

This is very similar to what RxSwift does with subscribe(...) and Variable. Here’s an example of what an observable array of strings would look like.

Now there’s one clear difference between my example and RxSwift’s example of an Observable. That’s the ‘DisposeBag’.


Introduction to "DisposeBag"

It was only after using my own Swift Observable implementation where I realized I was missing something critically important.

The observers are never deallocated. If you are observing the weather in a detail view controller and pop back to the root view, the detail view controller should be deallocated, along with anything owned by that view controller.

The problem with our Observable implementation is that after adding an observer to our Observable, we don’t know how to remove it when the observer’s associated object is deinitialized. We need a mechanism for the observable, like currentWeather above, to differentiate its active observers from observers that are no longer referenced anywhere.

Jared Sinclair brought up a great point about marking each observable with a token/unique key. Instead of just an observables array, we’ll use a dictionary that gives each observer a unique key; when a new observer is inserted, we return it’s key.

A bag is simply a collection of things that are not ordered, and can have repeated things in it, unlike a set. Similar to RxSwift, we’ll use a ‘DisposeBag’ that stores all of our observer’s unique keys. In addition, this bag needs to trigger the removal of every observer that is referenced in our bag from our Observable when it’s owner deallocates(a view controller in our example.)

That was a difficult sentence to understand, so I’ll break it down using our weather temperature above. When we start the application, our root view controller, VC1, subscribes one observer. When we push into the detail view controller, VC2, we subscribe another observer.

On subscribe, we return a Disposable, which is just an object that represents the key associated with that observer. We need to take that Disposable and place it a DisposeBag within VC2. It must be in the local scope of VC2 because when we pop the view controller and deinitialize VC2 and any references VC2 owns, our dispose bag needs to be on of them.

Implementing DisposeBag

class MyDisposeBag {
   
  var disposables: [Disposable] = []
   
  func add(_ disposable: Disposable) {
    disposables.append(disposable)
    print("new dispose bag count: \(disposables.count)")
  }
   
  func dispose() {
    disposables.forEach({$0.dispose()})
  }
   
  // When our view controller deinits, our dispose bag will deinit as well
  // and trigger the disposal of all corresponding observers living in the
  // Observable, which Disposable has a weak reference to: 'owner'.
  deinit {
    dispose()
  }
}

Conclusion and Looking Forward

And it works! We have a crude, but working Observable implementation written in Swift. It doesn’t leak memory and properly disposes of dereferenced closures. It allows multiple observers to subscribe to an Observable and enforces compile-time type safety by using generic properties and classes. Lastly, it doesn’t require pulling in a full-fledged reactive framework like RxSwift.

Check out the final example here Gist

In a future post, I’ll dive into some of the bells and whistles that come with using RxSwift! There’s a lot!

code

Related Posts

The Potential and Limitations of OpenAI's Custom GPTs.

I share my first impressions of building a custom GPT chatbot, delving into its limitations and questioning the longevity of GPTs reliant on file uploads. I wonder if custom GPTs are primarily intended as chat interfaces for APIs.

The Potential and Limitations of OpenAI's Custom GPTs.

Don't forget to deploy your CloudKit-backed SwiftData entities to production.

I thought SwiftData model changes would just work ✨. Make sure you press this button after updating your CloudKit-backed SwiftData models.

Don't forget to deploy your CloudKit-backed SwiftData entities to production.

7 months at Square.

Working at Square is awesome.

7 months at Square.