Chasing UIViewController retain cycles

A retain cycle is a common memory issue preventing objects to be released. It’s happens when two (or more) objects have a strong reference one to each other. The main object can not be released because the other object has a strong reference to it, and this other object will only be released when the main object is released. Dead end, these two objects will never be released.

This can lead to random crashes due to lack of memory on your users’ phone or weird behaviours in your app due to existing but not visible objects that are interfering with the normal execution.

Do I have a retain cycle?

That’s easy to test, in your class, add a print statement in the deinit method, also add a breakpoint. Assuming that your view controller is part of a navigation controller, run your app and go to the screen then hit back, you should hit the breakpoint and “MyViewController deinit called” should appear in your console. If this is not happening, then you view controller is part of a retain cycle and will never be released.

deinit {
    print("MyViewController deinit called")
}

Find and fix the retain cycles

We now know that a view controller generates retain cycles, let’s try to fix them. There are several reasons why a retain cycle exists.

I passed my view controller to someone else and he is retaining me

Typical example is with delegate. Let’s say we have this class that needs a delegate

MyCustomObject.swift

protocol MyCustomObjectDelegate {
   // some methods to be implemented
}

class MyCustomObject {
    var delegate: MyCustomObjectDelegate?
}

MyViewController.swift

let myCustomObject = MyCustomObject()
myCustomObject.delegate = self

To fix the retain cycle, we need to change this a little bit:

MyCustomObject.swift

protocol MyCustomObjectDelegate: class {
   // some methods to be implemented
}

class MyCustomObject {
    weak var delegate: MyCustomObjectDelegate?
}

Note that MyCustomObjectDelegate is now a class protocol and that the reference to this object is now weak.

Another case, slightly more subtle is possible, in this case, your delegate is not optional because you always need one and it always has to provide you with some values

MyCustomObject.swift

protocol MyCustomObjectDelegate {
   // some methods to be implemented
}

class MyCustomObject {
    private let delegate: MyCustomObjectDelegate

    init(delegate: MyCustomObjectDelegate) {
        self.delegate = delegate
    }
}

MyViewController.swift

let myCustomObject = MyCustomObject(delegate: self)

In that case, we will need to use the unowned keyword instead of the weak keyword:

MyCustomObject.swift
protocol MyCustomObjectDelegate: class {
   // some methods to be implemented
}

class MyCustomObject {
    private unowned let delegate: MyCustomObjectDelegate

    // init remains the same
}

A block / closure is retained

It’s also easy to generate a retain cycle when using closures. In the following example, the addInfiniteScrollWithHandler methods copies my block to eventually use it later. A copy automatically performs a retain on it, and if the block make use of self, then it’s incrementing my view controller retainCount.

tableView.addInfiniteScrollWithHandler { tableView in
    self.nextPageForScrollView(tableView)
}

The following one is similar, except that a method has been passed as the closure, this is nasty because the self is here implicit:

func infiniteScrollHandler(tableView: UITableView) {}
tableView.addInfiniteScrollWithHandler(infiniteScrollHandler)

To fix it, we need to use a safe version of self, using the [weak self] parameters in the closure. From now, all self. occurrences have to be replaced with self?. or self!.:

tableView.addInfiniteScrollWithHandler { [weak self] tableView in
    self?.nextPageForScrollView(tableView)
}

This can happen, but remember that most of the closures you are writing will probably not be retained! It’s not always necessary and therefore considered as overkill to write [weak self] and handle the optional self in all every closures. In the following example, the block will be executed directly and will not be retained, this will not generate a retain cycle.

UIView.animateWithDuration(0.25, animations: {
    self.view.alpha = 1.0
})

Leave a Reply

Your email address will not be published. Required fields are marked *