self captured by a closure before all members were initialized - but I did initialize them
Asked Answered
H

3

6

This is a toy example but it reduces exactly the situation I'm in:

class MyDataSource: UITableViewDiffableDataSource<String,String> {
    var string : String?
    init(string:String?) {
        self.string = string
        super.init(tableView: UITableView()) { (_, _, _) -> UITableViewCell? in
            print(self.string) // error
            return nil
        }
    }
}

I'm trying to make my table view data source self-contained, and my way of doing that (so far) is to subclass UITableViewDiffableDataSource. This works fine except when I try to give my subclass a custom initializer. The toy example shows the problem.

The way I want to populate the cell absolutely depends upon a value that can change later in the life of the data source. Therefore it cannot be hard-coded into the cell provider function. I cannot refer here simply to string, the value that was passed in the initializer; I must refer to self.string because other code is going to have the power to change this data source's string instance property later on, and I want the cell provider to use that new value when that happens.

However, I'm getting the error "self captured by a closure before all members were initialized". That seems unfair. I did initialize my string instance property before calling super.init. So it does have a value at the earliest moment when the cell provider method can possibly be called.

Halt answered 16/2, 2020 at 19:39 Comment(2)
This looks related.Danonorwegian
@MartinR Sure, I found lots of questions about this error message, but they didn't apply well to my situation (i.e. there was no solution given that I could use).Halt
D
8

While I'm not entirely sure why Swift doesn't allow this (something to do with capturing self to create the closure before the actual call to super.init is made), I do at least know of a workaround for it. Capture a weak local variable instead, and after the call to super.init set that local variable to self:

class MyDataSource: UITableViewDiffableDataSource<String,String> {
    var string : String?
    init(string:String?) {
        self.string = string
        weak var selfWorkaround: MyDataSource?
        super.init(tableView: UITableView()) { (_, _, _) -> UITableViewCell? in
            print(selfWorkaround?.string)
            return nil
        }

        selfWorkaround = self
    }
}

The only problem with this, though, is that if the closure is executed during the call to super.init, then selfWorkaround would be nil inside the closure and you may get unexpected results. (In this case, though, I don't think it is - so you should be safe to do this.)

Edit: The reason we make the local variable weak is to prevent the self object from being leaked.

Diathermy answered 16/2, 2020 at 20:12 Comment(3)
Very interesting idea. You're saying that selfWorkaround is captured by the closure and remains alive in closure-capture space — and so it continues to point, live, at self even though selfWorkaround was a local variable when it was captured.Halt
Yep! I tested it out just now to make sure I'm not crazy, and it indeed prints out what it would print out for self when that closure is called (after a call to apply on the data source object).Diathermy
Tested it out in my real use case and it works great (i.e. compiles and also runs correctly). That is seriously scary. The fact that I can safely "punch a hole" in the compiler's idea of self-protection here reinforces my suggestion is that the compiler is mistaken. I wonder if I should file a bug report. — Also, I have a funny feeling we're now leaking the MyDataSource. Just in case, I marked selfWorkaround as weak and nothing broke.Halt
R
5

you can access self via tableView.datasource and it will sort most of the problem.

Regime answered 16/2, 2020 at 20:9 Comment(5)
That's a very interesting idea. I'll give it a try. Of course, if that's legal, it rather suggests this really is a bug in Swift.Halt
It is legal and valid way. You are trying to buy-pass swift process of init. This is not a bug. You are finding address of something for whom address has not been allocated.Regime
The compiler error is what I'm suggesting is a bug. This initializer's closure, the CellProvider, should be @escaping; it won't be called until after initialization is finished, so, like lazy, computed variables, it should be permitted to reference self because self will exist when the time comes.Halt
The real bug / API-design-issue is that UITableViewDiffableDataSource requires CellProvider closure as an init() parameter, but that closure can't refer to self.anything ... Which means the CellProvider can't ask any questions about the data context when generating the cell... which is a dumb design. Should be able to init() the object, then set .cellProvider to a closure after super.init()Billybillycock
@BillPatterson Yeah, just like already happens with a section provider. Excellent point.Halt
B
2

Expanding on Abhiraj Kumar's answer from Feb 16 2020, here's an example of using the TableView provided to "reach back" to get the data source you attached to the table... i.e. "self":

class MyDataSource: UITableViewDiffableDataSource<String,String> {
    var string : String?
    init(string:String?) {
        self.string = string
        super.init(tableView: UITableView()) { (tableView, _, _) -> UITableViewCell? in
            
            // Very sketchy reach-through to get "self", forced by API design where
            // super.init() requires closure as a parameter
            let hack_self = tableView.dataSource! as! MyDataSource
            
            let selfDotStr = hack_self.string
            
            print("In closure, self.string is \(selfDotStr)")
            
            return nil // would return a real cell here in real application
        }
    }
}
Billybillycock answered 20/8, 2020 at 20:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.