1

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
            }
        }
    }
}
bobby123uk
  • 892
  • 4
  • 17

2 Answers2

1

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.)

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Are you sure @matt ? The function 'categorySelected' is within the 'viewDidLoad' lexical scope. Also, the compiler is no longer warning of a capture. – bobby123uk Jul 08 '20 at 10:31
  • Further, if I explicitly add 'self' to `categorySelected`, I get the error `MyVC` has no member `categorySelected`. – bobby123uk Jul 08 '20 at 10:35
  • Right, well done, didn't notice that. Corrected. Very nice indeed. – matt Jul 08 '20 at 14:52
1

I did some investigations, and indeed you get a retain cycle if you use an inner function that references self.

Here is an example:

typealias ClosureType = () -> ()
class Test {
    var closure:ClosureType?
    let value = 42
    
    func setClosure1() {
        self.closure = {
            print ("from setClosure1")
        }
    }
    
    func setClosure2() {
        self.closure = {
            [unowned self] in
            let v = self.value
            print ("from setClosure2 - value: \(v)")
        }
    }
    
    func setClosure3() {
        func innerFunc() {
            // [unowned self] in  // not allowed, compile error (sometimes even crashes)
            let v = value
            print ("value: \(v)")
        }
        self.closure = {
            [unowned self] in   // compiler warning: "Capture [self] was never used"
            print ("from setClosure3")
            innerFunc()
        }
    }

    deinit {
        print ("Deinit")
    }
}

If you use setClosure1 (trivial) or setClosure2 (capture clause), no retain cycle occurs:

if (1==1) {
    let t = Test()
    t.setClosure1()
    t.closure?()
}  // deinit called here

but if you call setClosure3, deinit will not be called:

if (1==1) {
    let t = Test()
    t.setClosure3()
    t.closure?()
}  // deinit NOT called here

There is no direct way to solve this problem; as you can see, using [unowned self] in the inner function results in an compiler error, and using it in setClosure3 results in a warning.

Nevertheless, there is a way to get around this issue - instead of using an inner function, you can use a second closure, in which you can specify the [unowned self] capture clause:

    func setClosure4() {
        let innerClosure:ClosureType = {
            [unowned self] in
            let v = self.value
            print ("value: \(v)")
        }
        self.closure = {
            print ("from setClosure4")
            innerClosure()
        }
    }

// ...

if (1==1) {
    let t = Test()
    t.setClosure4()
    t.closure?()
}  // deinit called here

Conclusion: Swift should allow capture clauses in innner functions.

Andreas Oetjen
  • 9,889
  • 1
  • 24
  • 34
  • Thanks @Andreas I ended up passing `self` as a parameter of the nested function, so I could take advantage of unowned self at the call site. – bobby123uk Jul 12 '20 at 07:57