This episode focuses on memory management. While memory management isn't the most exciting subject, it is one of the most important subjects of this series. Most developers new to RxSwift struggle with memory management. It isn't hard, but you need to learn and understand the basics.

An Example

I've created a simple project for this episode. It defines two view controllers, BlueViewController and GreenViewController. The application is as simple as it gets. Build and run the application in the simulator to take a look.

The application shows the user a blue view with a button at its center that reads Next. Tapping the button takes the user to another view controller with a green view. The green view has a label at its center that reads start. The user can return to the blue view by tapping the back button in the top left. It's a simple project, but it includes everything we need to learn about RxSwift and memory management.

A Simple Application

Creating an Observable

We first refresh your memory and apply what we covered in the previous episode. I'd like to create an observable to update the text the label displays in the green view. The text the label displays should increment every second. Open GreenViewController.swift and add an import statement for the RxSwift library.

import UIKit
import RxSwift

class GreenViewController: UIViewController {

    ...

}

Navigate to the viewDidLoad() method of the GreenViewController class. We create an observable of type Int by invoking the interval(_:scheduler:) method. The resulting observable emits a sequence of ascending integers. It emits an integer every second. Don't worry about the scheduler we pass to the interval(_:scheduler:) method. We cover schedulers later in this series.

override func viewDidLoad() {
    super.viewDidLoad()

    // Set Title
    title = "Green"

    // Configure View
    view.backgroundColor = UIColor(red: 0.47, green: 0.73, blue: 0.40, alpha: 1.0)

    Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.instance)
}

We subscribe to the observable by invoking the subscribe(onNext:onError:onCompleted:onDisposed:) method. In the onNext handler, we update the text property of the label, using the element of the next event. We also print the element of the next event to the console. In the onError handler, we print the error to the console. In the onCompleted and onDisposed handlers, we print the strings completed and disposed respectively. This should look familiar if you've watched the previous episode.

override func viewDidLoad() {
    super.viewDidLoad()

    // Set Title
    title = "Green"

    // Configure View
    view.backgroundColor = UIColor(red: 0.47, green: 0.73, blue: 0.40, alpha: 1.0)

    Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.instance)
        .subscribe(onNext: { (value) in
            self.label.text = "\(value)"
            print(value)
        }, onError: { (error) in
            print(error)
        }, onCompleted: {
            print("completed")
        }, onDisposed: {
            print("disposed")
        })
}

Build and run the application to see the result. Tap the next button and take a look at your first reactive user interface using RxSwift. Notice that the integers emitted by the observable are printed to the console.

A Reactive User Interface

Memory Leaks

Tap the back button in the top left to return to the blue view. We have barely used RxSwift and I'm sorry to say that we have already introduced a memory leak. Let's take a look at the problem. Notice that the integers emitted by the observable continue to be printed to the console. Let's dig a bit deeper. Click the Debug Memory Graph button in the debug bar at the bottom to inspect the application's memory graph.

The green view controller is popped from the navigation stack when the user taps the back button. The green view controller should be deallocated as a result, but that doesn't seem to be happening.

Memory Graph Debugger

Let's revisit the viewDidLoad() method of the GreenViewController class. The compiler warns us that the return value of the subscribe(onNext:onError:onCompleted:onDisposed:) method is unused. We resolve that warning later in this episode, but it's an indication that something's not quite right.

Compiler Warning

Let me explain what's happening. The subscribe(onNext:onError:onCompleted:onDisposed:) method creates a subscription and returns an object that conforms to the Disposable protocol. We talk more about the Disposable protocol in a minute. What you need to understand for now is that we are creating a reference cycle. The reference cycle prevents the green view controller from being deallocated.

The reference cycle is easy to understand. We create a subscription by invoking the subscribe(onNext:onError:onCompleted:onDisposed:) method. The observer keeps a strong reference to the observable it subscribes to and the observable keeps a strong reference to its observers. While the technical details are more complex, this simplification is sufficient to understand the underlying problem.

It's important to know and understand that the reference cycle is expected. It isn't a bug. It is how RxSwift works. The problem we are facing is that the reference cycle is never broken, which means that the observable and its observers are never deallocated. The result is a memory leak.

You may be wondering why the green view controller isn't being deallocated. It isn't involved in the reference cycle. That's easy to understand. Notice that self, the view controller, is strongly referenced in the onNext handler we pass to the subscribe(onNext:onError:onCompleted:onDisposed:) method. The observer and the observable keep each other alive. Because the observer keeps a strong reference to the view controller in its onNext handler, it also keeps the view controller alive.

Let's use a capture list to weakly reference the view controller in the onNext handler we pass to the subscribe(onNext:onError:onCompleted:onDisposed:) method.

override func viewDidLoad() {
    super.viewDidLoad()

    // Set Title
    title = "Green"

    // Configure View
    view.backgroundColor = UIColor(red: 0.47, green: 0.73, blue: 0.40, alpha: 1.0)

    Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.instance)
        .subscribe(onNext: { [weak self] (value) in
            self?.label.text = "\(value)"
            print(value)
        }, onError: { (error) in
            print(error)
        }, onCompleted: {
            print("completed")
        }, onDisposed: {
            print("disposed")
        })
}

We use optional chaining to access the label of the view controller in the onNext handler. Build and run the application. Navigate to the green view controller, wait for the label to update, and tap the back button in the top left. Click the Debug Memory Graph button in the debug bar at the bottom to inspect the application's memory graph. The green view controller is properly deallocated. We're making progress, but we're not quite there yet. Notice that the integers emitted by the observable continue to be printed to the console.

Disposing of Subscriptions

As I mentioned earlier, the reference cycle we created in the viewDidLoad() method is expected. However, the reference cycle needs to be broken when the subscription is no longer being used.

To break the reference cycle, we need to dispose of the subscription. When we dispose of a subscription, the observer no longer subscribes to the observable and it breaks its strong reference to the observable. This is important to release the resources of the subscription. Let me show you how that works.

We start by keeping a reference to the subscription. We define a private, variable property, disposable, of type Disposable?.

import UIKit
import RxSwift

class GreenViewController: UIViewController {

    // MARK: - Properties

    @IBOutlet var label: UILabel!

    // MARK: -

    private var disposable: Disposable?

    ...

}

We store a reference to the subscription in the disposable property. This also resolves the compiler warning.

override func viewDidLoad() {
    super.viewDidLoad()

    // Set Title
    title = "Green"

    // Configure View
    view.backgroundColor = UIColor(red: 0.47, green: 0.73, blue: 0.40, alpha: 1.0)

    disposable = Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.instance)
        .subscribe(onNext: { [weak self] (value) in
            self?.label.text = "\(value)"
            print(value)
        }, onError: { (error) in
            print(error)
        }, onCompleted: {
            print("completed")
        }, onDisposed: {
            print("disposed")
        })
}

To dispose of the subscription, we invoke dispose() on the subscription. The deinitializer of the GreenViewController class is a good place to do that.

import UIKit
import RxSwift

class GreenViewController: UIViewController {

    // MARK: - Properties

    @IBOutlet var label: UILabel!

    // MARK: -

    private var disposable: Disposable?

    // MARK: - Deinitialization

    deinit {
        disposable?.dispose()
    }

    ...

}

Build and run the application in the simulator, navigate to the green view controller, wait a few moments, and return to the blue view controller. No more integers are being printed to the console. This confirms that the observable and its observer were properly cleaned up when the green view controller was popped from the navigation stack.

Using a Dispose Bag

When I first learned that you need to keep track of subscriptions and manually dispose of subscriptions when they're no longer needed, I lost my appetite for reactive programming. I'm sure you agree that this should be automated in some way. While memory management isn't entirely automated when you work with RxSwift, once you understand how it works, it is virtually painless.

We first need to make a small change to the GreenViewController class. We invoked the dispose() method on the disposable in the deinitializer. Directly invoking the dispose() method is considered a bad practice, a code smell if you will. The recommended approach is to use a dispose bag.

A dispose bag is thread safe and the pattern is reminiscent of ARC or automatic reference counting. A dispose bag is nothing more than a container that references one or more subscriptions. When the dispose bag is deallocated, it loops over the subscriptions it manages and invokes the dispose() method on each subscription. Let's use a dispose bag in the GreenViewController class.

We remove the disposable property and replace it with a private, constant property, disposeBag. We create an instance of the DisposeBag class and store a reference in the disposeBag property. We also remove the deinitializer. We no longer need it.

import UIKit
import RxSwift

class GreenViewController: UIViewController {

    // MARK: - Properties

    @IBOutlet var label: UILabel!

    // MARK: -

    private let disposeBag = DisposeBag()

    ...

}

In the viewDidLoad() method, we keep a reference to the subscription in a constant with name disposable. We insert the disposable into the dispose bag by invoking the insert(_:) method on the dispose bag, passing in the disposable. That's it.

override func viewDidLoad() {
    super.viewDidLoad()

    // Set Title
    title = "Green"

    // Configure View
    view.backgroundColor = UIColor(red: 0.47, green: 0.73, blue: 0.40, alpha: 1.0)

    let disposable = Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.instance)
        .subscribe(onNext: { [weak self] (value) in
            self?.label.text = "\(value)"
            print(value)
        }, onError: { (error) in
            print(error)
        }, onCompleted: {
            print("completed")
        }, onDisposed: {
            print("disposed")
        })

    // Add to Dispose Bag
    disposeBag.insert(disposable)
}

The view controller no longer keeps a reference to the subscription. That's an improvement, but we can do better. Because adding a disposable to a dispose bag is such a common pattern, RxSwift exposes a convenience method to automatically add a disposable to a dispose bag. Let's first remove the disposable constant and the insert(_:) invocation.

To automatically add the disposable to the dispose bag, we invoke the disposed(by:) method on the disposable that is returned from the subscribe(onNext:onError:onCompleted:onDisposed:) method, passing in the DisposeBag instance.

override func viewDidLoad() {
    super.viewDidLoad()

    // Set Title
    title = "Green"

    // Configure View
    view.backgroundColor = UIColor(red: 0.47, green: 0.73, blue: 0.40, alpha: 1.0)

    Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.instance)
        .subscribe(onNext: { [weak self] (value) in
            self?.label.text = "\(value)"
            print(value)
        }, onError: { (error) in
            print(error)
        }, onCompleted: {
            print("completed")
        }, onDisposed: {
            print("disposed")
        }).disposed(by: disposeBag)
}

This looks pretty nice if you ask me. It's true that we need to keep track of the subscriptions we create, but the DisposeBag class makes that almost painless.

The view controller keeps a strong reference to the dispose bag. When the view controller is deallocated, the dispose bag is deallocated as a result because no other object keeps a strong reference to the dispose bag. Before the dispose bag is deallocated, it invokes dispose() on every disposable it keeps a reference to, breaking the reference cycles we discussed earlier.

Build and run the application in the simulator, navigate to the green view controller, wait a few moments, and return to the blue view controller. No more integers are being printed to the console. The observable and its observers were properly cleaned up when the green view controller was popped from the navigation stack.

The console also confirms that the dispose bag did its job. In the viewDidLoad() method of the GreenViewController class, we installed a disposed handler. That handler is invoked when the disposable is disposed of. The output in the console confirms this. The string disposed is printed to the console.

disposed

Stop Events

The resources of an observable are automatically released when it emits a stop event, that is, a completed event or an error event. This is fine because an observable is guaranteed to emit no other events when a completed or an error event is emitted. We covered that in the previous episode.

This is easy to test. Replace the interval(_:scheduler:) method in the viewDidLoad() method of the GreenViewController class with the timer(_:scheduler:) method. The observable emits a single next event after three seconds, followed by a completed event. After emitting the completed event, the disposed handler should be executed because the observable emitted a stop event.

override func viewDidLoad() {
    super.viewDidLoad()

    // Set Title
    title = "Green"

    // Configure View
    view.backgroundColor = UIColor(red: 0.47, green: 0.73, blue: 0.40, alpha: 1.0)

    Observable<Int>.timer(.seconds(3), scheduler: MainScheduler.instance)
        .subscribe(onNext: { [weak self] (value) in
            self?.label.text = "\(value)"
            print(value)
        }, onError: { (error) in
            print(error)
        }, onCompleted: {
            print("completed")
        }, onDisposed: {
            print("disposed")
        }).disposed(by: disposeBag)
}

Build and run the application in the simulator, navigate to the green view controller, and wait a few moments. The text of the label is set to 0 when the next event is emitted and the output in the console confirms that the completed and disposed handlers are both executed.

completed
disposed

What's Next?

Memory management is a key aspect of software development and it is important that you understand how RxSwift manages memory. Neglecting or ignoring memory management inevitably leads to performance problems so make sure you understand what we covered in this episode.