As a ReactiveSwift user myself, most of time, it’s difficult to convince someone to just simply start using it. The reality, for better or worse, is that most projects/teams are not ready to adopt it:

  1. The intrinsic problems of adding a big dependency.
  2. The learning curve.
  3. Adapting the current codebase to a FRP mindset/approach.

Nevertheless, a precious pattern can still be used, even without such an awesome lib like ReactiveSwift.

Enter Receiver!

Receiver is nothing more than an opinionated micro framework implementation of the Observer pattern (~120 LOC). Or, if you prefer, FRP without the F and a really small R (rP ).

Show me the codez!

Let’s begin with the basics. There are three methods in total. Yup, that’s right.

1. Creating the Receiver

let (transmitter, receiver) = Receiver<Int>.make()Copy the code

A receiver can never be created without an associated transmitter (what good would that be?)

2. Listening to an event

This is how you observe events:

Receiver. Listen {cheezburgers in print("Can I haz \(cheezburgers) cheezburger. 🐈")}Copy the code

As expected, you can do so as many times as you want:

Receiver. Listen {cheezburgers in print("Can I haz \(cheezburgers) cheezburger. 🐈")} receiver. Listen {cheezburgers in print  print("I have \(cheezburgers) cheezburgers and you have none!" )}Copy the code

And both handlers will be called, when an event is broadcasted.

3. Broadcasting an event

This is how you send events:

transmitter.broadcast(1)Copy the code

Opinionated, in what way?

Initializer.

The make method, follows the same approach used in ReactiveSwift, with pipe. Since a receiver only makes sense with a transmitter, it’s only logical for them to be created together.

Separation between the reader and the writer.

A lot of libs have the reader and the writer bundled within the same entity. For the purposes and use cases of this lib, it makes sense to have these concerns separated. It’s a bit like a UITableView and a UITableViewDataSource: one fuels the other, so it might be better for them to be split into two different entities.

sendLastValue and onlyNewValues

If you are familiar with FRP, you must have heard about cold and hot semantics. Receiver can’t really provide cold semantics, but it can provide something a bit more unusual called “warm” semantics. A “warm” Receiver, is a hot one, but provides the last value sent.

Hopefully this will make sense, with .sendLastValue (warm semantics):

let (transmitter, receiver) = Receiver<Int>.make(with: .sendLastValue)
transmitter.broadcast(1)

receiver.listen { wave in
    // This will be called with `wave == 1`
    // This will be called with `wave == 2`
}

transmitter.broadcast(2)Copy the code

With .onlyNewValues (hot semantics):

let (transmitter, receiver) = Receiver<Int>.make(with: .onlyNewValues)
transmitter.broadcast(1)

receiver.listen { wave in
    // This won't be called for `wave == 1`, since we only started listening after the first broadcast.
    // This will now be called with `wave == 2`, because we started listening before the second broadcast.
}

transmitter.broadcast(2)Copy the code

Ok, so why would I use this?

Well, to make your codebase awesome of course. There are a lot of places where the observer pattern can be useful. In the most simplistic scenario, when delegation is not good enough and you have an 1-to-N relationship.

A good use case for this would in tracking an UIApplication‘s lifecycle:

enum ApplicationLifecycle {
    case didFinishLaunching
    case didBecomeActive
    case didEnterBackground
}

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    private var transmitter: Receiver<ApplicationLifecycle>.Transmitter!

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

        let (transmitter, receiver) = Receiver<ApplicationLifecycle>.make()
        self.transmitter = transmitter
        // Pass down the `receiver` to where it's needed (e.g. ViewModel, Controllers)

        transmitter.broadcast(.didFinishLaunching)
        return true
    }

    func applicationDidEnterBackground(_ application: UIApplication) {
        transmitter.broadcast(.didEnterBackground)
    }

    func applicationDidBecomeActive(_ application: UIApplication) {
        transmitter.broadcast(.didBecomeActive)
    }
}Copy the code

Similar to the ApplicationLifecycle, the same approach could be used for MVVM:

class MyViewController: UIViewController { private let viewModel: MyViewModel private let transmitter: Receiver<UIViewControllerLifecycle>.Transmitter init(viewModel: MyViewModel, transmitter: Receiver<UIViewControllerLifecycle>.Transmitter) { self.viewModel = viewModel self.transmitter = transmitter super.init(nibName: nil, bundle: nil) } override func viewDidLoad() { super.viewdDidLoad() transmitter.broadcast(.viewDidLoad) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) transmitter.broadcast(.viewDidAppear) } override func viewDidDisappear(_ animated:  Bool) { super.viewDidDisappear(animated) transmitter.broadcast(.viewDidDisappear) } }Copy the code

The nice part is that the UIViewController is never aware of the receiver, as it should be.

At initialization time:

let (transmitter, receiver) = Receiver<UIViewControllerLifecycle>.make()
let viewModel = MyViewModel(with: receiver)
let viewController = MyViewController(viewModel: viewModel, transmitter: transmitter)Copy the code