2

I think lazy var {}() should run once. But today I meet this problem troubles me. And I have on any thought. I'm crying.

Here is reproduction class:

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

    private lazy var hotStockTableView: UITableView = {
        let tableView = UITableView()
        print("hotStockTableView ======> \(tableView) \n")
        tableView.delegate = self
        tableView.dataSource = self
        // Here cause crash.
        tableView.tableFooterView = UIView() // if comment this line, everthing is ok.
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
        return tableView
    }()


    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(hotStockTableView)

        hotStockTableView.frame = view.bounds
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        print("tableView =====> \(tableView) \n")


        if tableView == hotStockTableView {
            return 5
        } else {
            return 0
        }
    }

    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        var cell = tableView.dequeueReusableCell(withIdentifier: "cell")
        if cell == nil {
            cell = UITableViewCell(style: .default, reuseIdentifier: "cell")
        }
        cell?.textLabel?.text = "\(indexPath.row)"
        return cell!
    }
}

Could someone help me?

Callam
  • 11,409
  • 2
  • 34
  • 32
jkyin
  • 168
  • 1
  • 12

2 Answers2

6

The problem is that your closure to create a table view ends up being recursive.

  • When you set a tableFooterView on the table, it immediately tries to find out if that footer should be drawn / visible / onscreen immediately.

  • To know if the footer should be visible, the table view has to find out how many rows should currently be displayed, so it asks the data source (your view controller) for the number of rows in the the first section

  • Inside your method tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int, you have the line if tableView == hotStockTableView {. This tries to access the lazy hotStockTableView var, which still hasn't completed initializing, since that closure is what is calling this code in the first place

  • So the lazy var closure is kicked off again and recursively goes through the same steps over and over and over until stack overflow and crash.

Fortunately, the solution is easy. If you haven't set the data source yet then adding the table view footer will never call your number of rows data source method. So just change the order of code in your lazy closure to the following:

private lazy var hotStockTableView: UITableView = {
    let tableView = UITableView()
    print("hotStockTableView ======> \(tableView) \n")
    tableView.tableFooterView = UIView()
    tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
    tableView.delegate = self
    tableView.dataSource = self
    return tableView
}()

Now your code will work just fine.

Daniel Hall
  • 13,457
  • 4
  • 41
  • 37
  • Thanks your answer. Completely solved my problem. If so, whether it is the safest way to to set delegate and dataSource at last line always inside closure code block to avoid anything other issue? – jkyin Feb 28 '18 at 06:59
  • @jkyin yes, it's definitely safest to set delegate and datasource as the last steps, because there are a number of possible recursive triggers that can happen if those are set earlier! – Daniel Hall Mar 01 '18 at 04:12
  • @jkyin if this answer solved your question please mark it as the correct answer. – ScottyBlades Jan 09 '21 at 18:03
3

About lazy variables there is a good answer on SO:

As far as thread safety is concerned, lazy var are not thread safe in Swift.

That means if two different threads try to access the same lazy var at the same time, before such variable has been initialized it is possible that one of the threads will access partially constructed instance.

I'd suggest trying to set delegate and dataSource after the lazy view is created. It might be accessed by delegate or dataSource before it is fully initialized.

iWheelBuy
  • 5,470
  • 2
  • 37
  • 71