Search code examples
iosswiftuitableviewcell

WHY ALWAYS initialize 19 cells?


I'm learning about TableView in Swift. My teacher give me this question: "You have 20 cells, but the screen shows 3 cell. How many cells have been initialized, and why?

This is my code. I tried with more than 20 cells, it's always initialized 19 cells. Can someone explain to me why? Thank you very much I test on iPhone 11 ProMax, iOS 13.1

override func viewDidLoad() {
        super.viewDidLoad()
        myTable.register(UINib(nibName: "ContactCell", bundle: nil), forCellReuseIdentifier: "ContactCell")
        myTable.dataSource = self
        myTable.delegate = self
    }
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 20

    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "ContactCell", for: indexPath) as! ContactCell
        cell.textLabel?.text = "Superhero"

        return cell
    }
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return myTable.frame.size.height / 3.0
    }
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        print(indexPath.section)
        print(indexPath.row)
    }

This code on another file

    class ContactCell: UITableViewCell {

        @IBOutlet weak var cellShow: UIView!
        override func awakeFromNib() {
            super.awakeFromNib()
            print("awakeFromNib")
        }

        override func setSelected(_ selected: Bool, animated: Bool) {
            super.setSelected(selected, animated: animated)

        }  
    }

Solution

  • From Apple's Docs (https://developer.apple.com/documentation/uikit/uitableviewdelegate/1614998-tableview):

    Before it appears onscreen, the table view calls this method for the items in the visible portion of the table. As the user scrolls, the table view calls the method for items only when they move onscreen. It calls the method each time the item appears onscreen, regardless of whether it appeared onscreen previously.

    During layout, the table view will start populating. During this process, sizes for elements are being determined. Depending on the initial height of your cell and the height of the table view, heightForRowAt can be called first for rows that may appear, then for rows that probably will appear, then for rows that will appear (and rows that may be just-off-screen).

    Add a print() statement to your heightForRowAt func as follows:

    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    
        // add this line
        print(indexPath)
    
        return myTable.frame.size.height / 3.0
    }
    

    On my quick test, if 7 default height rows would fit based on my table view's height, I see this in the debug output:

    [0, 0]
    [0, 0]
    [0, 0]
    [0, 1]
    [0, 1]
    [0, 1]
    [0, 2]
    [0, 2]
    [0, 2]
    [0, 3]
    [0, 3]
    [0, 3]
    [0, 4]
    [0, 4]
    [0, 4]
    [0, 5]
    [0, 5]
    [0, 5]
    [0, 6]
    [0, 6]
    [0, 6]
    

    As you can see, 7 cells are being initialized, with heightForRowAt being called 3 times for each cell.

    Edit: ugh... zero-based... changed wrong 6 to correct 7


    Additional notes...

    If you set .estimatedRowHeight, for example:

    myTable.estimatedRowHeight = 150
    

    you will see fewer cells being initialized, and with:

    myTable.estimatedRowHeight = 10
    

    you will see more cells being initialized, as auto-layout will use that value as the initial row heights rather than the default row height.

    Also, if you want to understand a bit more about how cells are reused, add this to your didSelectRowAt handler (this is just a debug / dev feature, probably not useful for production code):

    let reusableCells = tableView.value(forKey: "_reusableTableCells")
    print(reusableCells as Any)
    

    In my case (without setting .estimatedRowHeight), I get:

    Optional({
        ContactCell =     (
            "<MyTest.ContactCell: 0x7fd0ccc295b0; baseClass = UITableViewCell; frame = (0 300; 295 100); text = 'Superhero'; clipsToBounds = YES; hidden = YES; autoresize = W; layer = <CALayer: 0x6000010fecc0>>",
            "<MyTest.ContactCell: 0x7fd0ccc2a2a0; baseClass = UITableViewCell; frame = (0 400; 295 100); text = 'Superhero'; clipsToBounds = YES; hidden = YES; autoresize = W; layer = <CALayer: 0x6000010ff0c0>>",
            "<MyTest.ContactCell: 0x7fd0ccc2ad90; baseClass = UITableViewCell; frame = (0 500; 295 100); text = 'Superhero'; clipsToBounds = YES; hidden = YES; autoresize = W; layer = <CALayer: 0x6000010ff4a0>>",
            "<MyTest.ContactCell: 0x7fd0ccc2ba80; baseClass = UITableViewCell; frame = (0 600; 295 100); text = 'Superhero'; clipsToBounds = YES; hidden = YES; autoresize = W; layer = <CALayer: 0x6000010ff8a0>>"
        );
    })
    

    Note that the number of cells in the "reusable pool" will change as you scroll up and down.

    Another Edit:

    Also worth experimenting with... change this line in cellForRowAt, from:

    cell.textLabel?.text = "Superhero"
    

    to:

    cell.textLabel?.text = "Superhero \(indexPath.row)"
    

    Now, when selecting a row and printing out the "reusableCells" you can see which cells are actually in the "pool." My result changes to:

    Optional({
        ContactCell =     (
            "<MyTest.ContactCell: 0x7fef77608330; baseClass = UITableViewCell; frame = (0 300; 295 100); text = 'Superhero 3'; clipsToBounds = YES; hidden = YES; autoresize = W; layer = <CALayer: 0x600003d36d20>>",
            "<MyTest.ContactCell: 0x7fef776091b0; baseClass = UITableViewCell; frame = (0 400; 295 100); text = 'Superhero 4'; clipsToBounds = YES; hidden = YES; autoresize = W; layer = <CALayer: 0x600003d37120>>",
            "<MyTest.ContactCell: 0x7fef7760a030; baseClass = UITableViewCell; frame = (0 500; 295 100); text = 'Superhero 5'; clipsToBounds = YES; hidden = YES; autoresize = W; layer = <CALayer: 0x600003d37500>>",
            "<MyTest.ContactCell: 0x7fef7760aeb0; baseClass = UITableViewCell; frame = (0 600; 295 100); text = 'Superhero 6'; clipsToBounds = YES; hidden = YES; autoresize = W; layer = <CALayer: 0x600003d37920>>"
        );
    })
    

    Showing me that cells for rows 4, 5, 6 and 7 (as shown in the text = property --- again, zero-based) were initialized and are in the "reuse pool".