iosswiftretain-cycle

Am I capturing self in this nested function? The compiler does not fire a warning


I can't find any official documentation on this and there's mixed opinions out there.

In the following situation, all is well.

final class MyVC: UIViewController {
    
    var space: Space!
    
    private let tableView = MenuCategoriesTableView()
    
    private let tableViewHandler = MenuCategoriesTableViewHandler()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        tableView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(tableView)
        NSLayoutConstraint.activate([
            tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            tableView.topAnchor.constraint(equalTo: view.topAnchor),
            tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
        
        tableView.dataSource = tableViewHandler
        tableView.delegate = tableViewHandler
        
        tableViewHandler.didSelectRow = { [unowned self] option in
            let category = option.makeCategory()
            if category.items.count > 0 {
                let controller = MenuItemsViewController()
                controller.title = option.rawValue
                controller.space = self.space
                self.show(controller, sender: self)
            } else {
                // whatever
            }
        }
    }
}

However, if I make the following change, I no longer need to use unowned self, but I'm still concerned about capture self. Should I be concerned? If not, why?

final class MyVC: UIViewController {
    
    ...etc...
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        ...etc...
        
        func categorySelected(_ option: MenuOption, _ category: MenuCategory) {
            let controller = MenuItemsViewController()
            controller.title = option.rawValue
            controller.space = space
            show(controller, sender: self)
        }
        
        tableViewHandler.didSelectRow = { option in
            let category = option.makeCategory()
            if category.items.count > 0 {
                categorySelected(option, category)
            } else {
                // whatever
            }
        }
    }
}

Solution

  • When you assign a closure to tableViewHandler.didSelectRow, you assign to it and retain whatever that closure captures.

    self is retaining tableViewHandler.

    Therefore, the danger is that you will refer to self within the closure. If you do, that's a retain cycle.

    And this might not be due to referring to self explicitly. Any mention of a property or method of self is an implicit reference to self.


    Okay, so with that out of the way, let's examine the closure.

    You do not mention self implicitly or explicitly in the body of the closure. However, you do call a local method, categorySelected. Therefore, you capture this method.

    And categorySelected does mention self. Therefore, it captures self (because every function is a closure).

    Thus there is a potential retain cycle and you should continue to say unowned self to prevent a retain cycle.

    (I presume that the compiler can't help you here by warning of the retain cycle; there's too much complexity. It's a problem you just have to solve by human reason.)