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.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • [This](https://stackoverflow.com/a/48996695/1187415) looks related. – Martin R Feb 16 '20 at 21:38
  • @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). – matt Feb 16 '20 at 22:09

3 Answers3

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.

TylerP
  • 9,600
  • 4
  • 39
  • 43
  • 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. – matt Feb 16 '20 at 20:15
  • 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). – TylerP Feb 16 '20 at 20:16
  • 1
    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. – matt Feb 16 '20 at 20:29
5

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

pableiros
  • 14,932
  • 12
  • 99
  • 105
Abhiraj Kumar
  • 160
  • 1
  • 6
  • 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. – matt Feb 16 '20 at 20:12
  • 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. – Abhiraj Kumar Feb 16 '20 at 20:17
  • 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. – matt Feb 17 '20 at 01:01
  • 4
    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() – Bill Patterson Aug 20 '20 at 20:27
  • @BillPatterson Yeah, just like already happens with a section provider. Excellent point. – matt Oct 01 '20 at 18:55
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
        }
    }
}
Bill Patterson
  • 2,495
  • 1
  • 19
  • 20