Retain cycle with Swift closure context

I’m developing a Swift iOS app using MVVM. I noticed yesterday that my ViewModel was being retained by a “Swift closure context.”


I’ve made an example app so you can see the problem for yourselves. It is here: https://www.dropbox.com/s/kn23lptixk25acq/LeakTest.zip


Instructions: Run the app, wait 5 seconds for the BlueViewController to be replaced by the RedViewController and then hit the Debug Memory Graph button. Observe that BlueViewModel is retained.


Do I need to be worried about this kind of retain cycle? Will the retain cycle be broken eventually?

Answered by DTS Engineer in 344583022

Do I need to be worried about this kind of retain cycle?

Yes.

Will the retain cycle be broken eventually?

No.

For the sake of OOPer and others, here’s a snippet of your code, slightly edited to simplify things:

class BlueViewModel: NSObject {
    var updateUI: (() -> Void)!
    var titleText: String {
        return "..."
    }
}

class BlueViewController: UIViewController {

    var viewModel: BlueViewModel!

    init(viewModel: BlueViewModel) {
        self.viewModel = viewModel
        super.init(nibName: "BlueViewController", bundle: nil)
        viewModel.updateUI = { [weak self] in
            self?.titleLabel.text = viewModel.titleText
        }
    }

    … more stuff …
}

You’ve done the weak self dance in

BlueViewController
, so that there’s nothing to stop that getting deallocated. However, you’ve still get a retain cycle between your closure (lines 15 through 17) and the
viewModel
object itself. Specifically, line 16 references the
viewModel
local variable from line 12, not the
viewModel
property from line 10.

There’s a bunch of ways you can fix this. Probably the easiest is to change line 16 to this:

self?.titleLabel.text = self?.viewModel.titleText

At that point the closure is only capturing

self
and thus doesn’t retain the view model directly.

However, I think it would be better to rethink your strategy a bit because the current structure just leaves potential footguns lying around your project. One approach I often use is to have the closure pass a ‘self’ value to the callback, which rules out a whole class of problems like this. For example:

class BlueViewModel: NSObject {
    var updateUI: ((BlueViewModel) -> Void)!
    …
}

Alas this won’t work in your real project because

BlueViewModel
is a subclass. These days I avoid that by avoiding inheritance, using structs for my model ‘objects’, but that’s a pretty big change of direction.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

You should better include the core part of your code as text. Many readers are not allowed to download online resources.


Generally, if you observe unexpectedly retained instances between multiple view controllers, the retain cycle would very likely be problematic.

Accepted Answer

Do I need to be worried about this kind of retain cycle?

Yes.

Will the retain cycle be broken eventually?

No.

For the sake of OOPer and others, here’s a snippet of your code, slightly edited to simplify things:

class BlueViewModel: NSObject {
    var updateUI: (() -> Void)!
    var titleText: String {
        return "..."
    }
}

class BlueViewController: UIViewController {

    var viewModel: BlueViewModel!

    init(viewModel: BlueViewModel) {
        self.viewModel = viewModel
        super.init(nibName: "BlueViewController", bundle: nil)
        viewModel.updateUI = { [weak self] in
            self?.titleLabel.text = viewModel.titleText
        }
    }

    … more stuff …
}

You’ve done the weak self dance in

BlueViewController
, so that there’s nothing to stop that getting deallocated. However, you’ve still get a retain cycle between your closure (lines 15 through 17) and the
viewModel
object itself. Specifically, line 16 references the
viewModel
local variable from line 12, not the
viewModel
property from line 10.

There’s a bunch of ways you can fix this. Probably the easiest is to change line 16 to this:

self?.titleLabel.text = self?.viewModel.titleText

At that point the closure is only capturing

self
and thus doesn’t retain the view model directly.

However, I think it would be better to rethink your strategy a bit because the current structure just leaves potential footguns lying around your project. One approach I often use is to have the closure pass a ‘self’ value to the callback, which rules out a whole class of problems like this. For example:

class BlueViewModel: NSObject {
    var updateUI: ((BlueViewModel) -> Void)!
    …
}

Alas this won’t work in your real project because

BlueViewModel
is a subclass. These days I avoid that by avoiding inheritance, using structs for my model ‘objects’, but that’s a pretty big change of direction.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"
Retain cycle with Swift closure context
 
 
Q