skip to Main Content

I was reading retain cycle and tried the following code which should in theory leak memory but when I use Instruments, it does not show any memory leaks

import UIKit

class ViewController: UIViewController {

    private var button = {
        let _button = UIButton()
        _button.setTitle("Tap Me", for: .normal)
        _button.setTitleColor(.systemBlue, for: .normal)
        return _button
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.addSubview(button)
        
        button.addTarget(self, action: #selector(actionButton), for: .touchUpInside)
        
        button.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            button.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            button.widthAnchor.constraint(equalToConstant: 80),
            button.heightAnchor.constraint(equalToConstant: 30)
        ])
    }
    
    @objc private func actionButton() {
        
        let secondVC = ViewController2()
        
        present(secondVC, animated: true)
    }

}

class MyView: UIView {
    
    let viewContoller: ViewController2
    
    init(from vc: ViewController2) {
        viewContoller = vc
        
        super.init(frame: .zero)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
}

class ViewController2: UIViewController {
    
    var myView: MyView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .red
        
        myView = MyView(from: self)
    }
    
}

There’s a button on ViewController which upon tapping opens a new view controller ViewController2 which initialises a UIView and when I dismiss that view controller(ViewController2), it should create a retain cycle (since the view also holds ViewController2 object). Where exactly am I going wrong?

Edit 1: I tried the deinit inside the ViewController2 and it worked as expected. However, the confusion started when I tried to profile the same code on Instruments app and it showed "No Leaks", green ticks all the way

2

Answers


  1. You have successfully created a retain cycle. Your only problem is how to detect it. To detect the retain cycle, add these lines to MyView and ViewController2, respectively:

    deinit {
        print("farewell from MyView")
    }
    
    deinit {
        print("farewell from ViewController2")
    }
    

    Now run your app, tap the button, and dismiss (by dragging) the presented view controller. Nothing prints to the console. Thus we know that neither ViewController2 nor MyView are being deallocated. That’s the leak.


    It will also help to make a contrasting situation so that you can see that the "farewell" messages would print if there were not a retain cycle. To make it, change MyView like this:

    let viewContoller: ViewController2?
    
    init(from vc: ViewController2?) {
        viewContoller = vc
    
        super.init(frame: .zero)
    }
    

    Now change

        myView = MyView(from: self)
    

    to

        myView = MyView(from: nil)
    

    Run the app, tap the button, and dismiss (by dragging) the presented view controller. The deinit messages print! Thus we have created a red-green test that demonstrates the retain cycle. You can change the code back to

        myView = MyView(from: self)
    

    and confirm that the deinit message do not print. Have fun playing with your brand new retain cycle demonstration app!

    Login or Signup to reply.
  2. You are in fact creating a retain cycle, you can either check the debug memory graph after dismissing ViewController2 and the instance will be there: 1

    Or you can implement the deinit method and log something:

    deinit { print("Instance removed from memory") }
    

    In any case, with the code you provided the instance will be showing in the memory graph and the deinit method won’t be called in either the view nor the controller 2.

    In order to break the cycle you would need a reference to be weak (or unowned) like so:

    weak var viewContoller: ViewController2?
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search