This episode is freely available thanks to the support of our subscribers

Subscribers get exclusive access to new and all previous subscriber-only episodes, video downloads, and 30% discount for team members. Become a Subscriber

We extend the Future type of a previous episode to a simple reactive library. Along the way, we dive into debugging a reference cycle in our implementation.

00:06 In this episode, we're going to build upon the Future type we implemented in a previous episode. Specifically, we'll use the Future type as a basis to build a simple reactive implementation. While a future is an abstraction around a one-off callback, a signal in a reactive library models values over time.

00:38 You'd use a future in an asynchronous API if the call only delivers one result, e.g. most network calls or some expensive computation. In contrast, you'd use a signal if you have to handle multiple values over time, like for a button that can be tapped multiple times, or for a text field that changes its value with each new user input. In the case of the signal, we have to think about some additional issues, like memory management (to avoid reference cycles), and subscription management (to allow clients to opt in and opt out of receiving values of a particular signal).

01:17 The motivation behind writing a small reactive library from scratch is really to understand the concept of reactive programming on a deeper level. Our goal isn't to write production-ready code, but rather to wrap our heads around what's going on behind the scenes when using one of the existing reactive libraries.

Turning Future into Signal

01:53 We start out with the Future type from episode #36:

final class Future<A> {
    var callbacks: [(Result<A>) -> ()] = []
    var cached: Result<A>?
    
    init(compute: (@escaping (Result<A>) -> ()) -> ()) {
        compute(self.send)
    }
    
    private func send(_ value: Result<A>) {
        assert(cached == nil)
        cached = value
        for callback in callbacks {
            callback(value)
        }
        callbacks = []
    }
    
    func onResult(callback: @escaping (Result<A>) -> ()) {
        if let value = cached {
            callback(value)
        } else {
            callbacks.append(callback)
        }
    }
}

01:55 As a first step, we remove the caching of the result, because this doesn't make sense for a signal anymore, at least not in this form. We also remove the initializer, rename the type from Future to Signal, make send public for now, and rename onResult to subscribe. Lastly, we don't clear out the callbacks at the end of the send method, since we now want to be able to send multiple values. This simplifies the code quite a bit:

final class Signal<A> {
    var callbacks: [(Result<A>) -> ()] = []
    
    func send(_ value: Result<A>) {
        for callback in callbacks {
            callback(value)
        }
    }
    
    func subscribe(callback: @escaping (Result<A>) -> ()) {
        callbacks.append(callback)
    }
}

03:31 Now we can try out the Signal:

let signal = Signal<String>()
signal.subscribe { result in
    print(result)
}
signal.send(.success("Hello World"))

04:30 The Signal implementation is now very simple, but also very limited. We'll have to add back some more complexity once we build out this type further.

Separating Send/Subscribe APIs

04:49 The first enhancement we'll make is to separate the Signal's API into a sending part and a receiving part. This is a very common pattern in reactive libraries, because it allows you to control who can send new values into the signal vs. who's only allowed to subscribe.

05:22 For this we add a static method, pipe, to Signal, which returns those two things: a function with which we can send new values, and the signal itself, which is read-only. You can take the name of the method quite literally: the returned tuple can be thought of as a physical pipe with two ends. At one end you can insert new values (also called the "sink"), and at the other end you can observe what's coming out:

final class Signal<A> {
    // ...
    
    static func pipe() -> ((Result<A>) -> (), Signal<A>) {
        let signal = Signal<A>()
        return (signal.send, signal)
    }

    private func send(_ value: Result<A>) {
        // ...
    }
}

06:43 With the pipe method in place, we were able to make send private again. The ability to send new values now only gets exposed via the pipe method.

07:06 To try out these changes, we just have to modify our test code a little bit:

let (sink, signal) = Signal<String>.pipe()
signal.subscribe { result in
    print(result)
}
sink(.success("Hello World"))

Adding a Signal to a Text Field

07:30 Let's put all of this code into a more realistic context. For this purpose, we're going to fake a view controller in our playground and subscribe to a text field's signal in viewDidLoad:

class VC {
    let textField = NSTextField()
    var stringSignal: Signal<String>?
    
    func viewDidLoad() {
        stringSignal = textField.signal()
        stringSignal?.subscribe {
            print($0)
        }
    }
}

var vc: VC? = VC()
vc?.viewDidLoad()
vc?.textField.stringValue = "17"

09:05 To make this code work, we have to add the signal method in an extension to NSTextField. In this method, we can take advantage of the send/subscribe separation we've created above — the text field is able to send new values to the signal, while any other code can only observe:

extension NSTextField {
    func signal() -> Signal<String> {
        let (sink, result) = Signal<String>.pipe()
        KeyValueObserver(object: self, keyPath: #keyPath(stringValue)) { str in
            sink(.success(str))
        }
        return result
    }
}

10:23 In this approach, we don't want the text field to reference the signal, but rather the signal should reference the text field (via the key-value observer). This has the advantage that it's more flexible, since we'd have to subclass the text field or use associated objects to store a reference to the signal in the text field itself.

12:09 The KeyValueObserver class is a simple wrapper around KVO that calls a function each time the observed property changes. It also manages the observation lifetime: once the key-value observer instance goes away, it stops observing the text field. As long as the key-value observer is alive, the observed object is guaranteed to be around as well, since the observer instance holds a strong reference to the observed object.

12:39 Since we're not yet holding on to the KeyValueObserver instance, it gets deinited immediately and observation stops. The easiest way to fix this is to just store the observer in a property of the signal. We'll call this property objects for now:

final class Signal<A> {
    var objects: [Any] = []
    // ...
}

extension NSTextField {
    func signal() -> Signal<String> {
        let (sink, result) = Signal<String>.pipe()
        let observer = KeyValueObserver(object: self, keyPath: #keyPath(stringValue)) { str in
            sink(.success(str))
        }
        result.objects.append(observer)
        return result
    }
}

15:16 Now the observer will be alive as long as the signal is alive. Since the signal is stored in a property of the view controller, new values will be sent as long as the view controller is around.

Dealing with Reference Cycles

15:33 Unfortunately, we accidentally introduced a reference cycle, which makes it so that the signal will never be deallocated.

15:46 Reference cycles are difficult to debug sometimes, since many objects can be involved in creating the cycle. A very simple technique to help diagnosing these cycles is to add a print statement to a class' deinit. For example, we can try this with our view controller class:

class VC {
    // ...
    deinit {
        print("Removing vc")
    } 
}

var vc: VC? = VC()
vc?.viewDidLoad()
vc?.textField.stringValue = "17"
vc = nil

Once we set the vc variable to nil, the view controller gets deallocated and "Removing vc" is printed in the console.

16:33 However, when we add the same debug code to the Signal type, it doesn't get printed in the console. This clearly shows that the signal instance never gets deallocated:

final class Signal<A> {
    // ...
    deinit {
        print("deiniting signal")
    }
}

Xcode's Memory Debugger

16:57 One very useful tool to further diagnose the problem is Xcode's memory debugger. Since this doesn't work in a playground, we'll just copy/paste our code into a command line project and continue working in there.

17:21 Now we set a break point after we've set the vc variable to nil and run the project. Once the debugger stops on this line, we can open the memory debugger:

The reference cycle in Xcode's memory debugger

17:40 In the sidebar we see all the objects that are alive, e.g. the Signal<String> instance that shouldn't be around anymore. On the right-hand side, we see all the objects involved in the reference cycle that keeps the signal alive: the signal references the key-value observer through its objects property, and the key-value observer references the signal via a few more intermediate steps.

18:16 This visualization can be very helpful in diagnosing the problem. However, sometimes the memory debugger isn't able to generate a graph like this, but it'll still show the objects that are alive in the sidebar.

Sketching Out Reference Cycles

19:04 Another helpful approach is to sketch out the references of all the objects on a piece of paper. We do this a lot to understand how we've created a reference cycle. In the example at hand, the diagram looks like this:

Sketching out object references

19:10 In the upper-left corner we have the view controller, which strongly references the text field and the string signal (as indicated by the arrows). The string signal references the closure with the print statement, as well as the key-value observer it uses to observe the text field for changes. The key-value observer in turn holds a strong reference to the text field.

19:40 This might already look like a reference cycle when you look at the left part of the diagram (view controller – string signal – key-value observer – text field – view controller). However, not all references are pointing in the same direction, so we don't have a problem yet.

19:55 The key-value observer also references the closure that gets called when the observed property changes. In our case, this is the function that feeds the new values into the sink. In turn, the sink strongly references the signal, since the sink is just the signal's send method. So here's our reference cycle.

Fixing the Reference Cycle

20:14 To fix the cycle, we have to think about which of the references could be a weak reference. For example, we can't make the reference from the signal to the observer weak, since the observer wouldn't be referenced strongly by anything anymore. The same goes for the closure that's referenced by the observer. Therefore, we're going to break the cycle by making the reference from the closure to the signal weak:

The object graph with the reference cycle fixed

20:40 For this, we have to modify our implementation of the Signal's pipe method. Instead of returning signal.send, we're going to wrap this in a closure that captures signal weakly:

static func pipe() -> ((Result<A>) -> (), Signal<A>) {
    let signal = Signal<A>()
    return ({ [weak signal] value in signal?.send(value) }, signal)
}

21:50 With this minor change, the reference cycle is fixed, which we can see from the printout in the console "Removing signal", as well as in the memory debugger.

22:31 In this case, the reference cycle was caused by a mistake in the implementation of Signal. However, there are also cases where it's the responsibility of the consumer of the API to not create cycles. For example, if we were to refer to self in the closure we pass to the subscribe call in viewDidLoad, we would've created a reference cycle between the view controller and the signal. To avoid that, we'd have to specify weak self or unowned self in the closure's capture list.

23:27 With this problem out of the way, we'll add a bit more functionality to our Signal type next time!

Recent Episodes

See All

Unlock Full Access

Subscribe to Swift Talk

  • Watch All Episodes

    A new episode every week

  • icon-benefit-download Created with Sketch.

    Download Episodes

    Take Swift Talk with you when you're offline

  • Support Us

    With your help we can keep producing new episodes