Home > Back-end >  Scroll tableView when there is no enough cells
Scroll tableView when there is no enough cells

Time:12-16

I have a UITableView that for some reasons, I set a contentInset.top and contentOffset.yon it in the ViewDidLoad

tableView.contentInset.top = 150
tableView.contentOffset.y = -150

The concept is, when the it gets opened, the rows start -150 point from the top, and when scroll begins, the rows come back to the top first, and then the actual scrolling starts (new cells appears from bottom and old cell disappear in the top).

The only issue is, when there isn't enough cell on the UITableView, it won't scroll to back to the top. I actually don't care about the actual scrolling starts (new cells appears from bottom and old cell disappear in the top), I want that in any case with any number of cells, the table view scroll to the top like that:

tableView.contentInset.top = 0
tableView.contentOffset.y = 0

and then when there is no enough cell, it won't go for the actual scrolling. Is there any way to do that?

BTW, I use scrollViewDidScroll to smoothly move it up and down with user finger, want to do that when there is no enough cell

enter image description here

Thank you so much

CodePudding user response:

What you want to do is set the table view's .contentInset.bottom if the resulting height of the rows is less than the height of the table view's frame.

We'll start with a simple dynamic height cell (a multiline label):

class DynamicHeightCell: UITableViewCell {
    
    let theLabel: UILabel = {
        let v = UILabel()
        v.numberOfLines = 0
        v.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
        return v
    }()
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    
    func commonInit() {
        theLabel.translatesAutoresizingMaskIntoConstraints = false
        contentView.addSubview(theLabel)
        let g = contentView.layoutMarginsGuide
        NSLayoutConstraint.activate([
            theLabel.topAnchor.constraint(equalTo: g.topAnchor),
            theLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
            theLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            theLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor),
        ])
    }
}

and a basic controller with a table view, inset by 40-points from each side:

class TableInsetVC: UIViewController, UITableViewDelegate, UITableViewDataSource {
    
    // set the number of rows to use
    //  once we get past 7 (or so), the rows will be
    //  taller than the tableView frame
    let testRowCount = 5
    
    let tableView: UITableView = {
        let v = UITableView()
        return v
    }()
    
    // we'll cycle through colors for the cell backgrounds
    //  to make it easier to see the cell frames
    let bkgColors: [UIColor] = [
        .systemRed, .systemGreen, .systemBlue, .systemYellow, .systemCyan, .systemBrown,
    ]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemBackground
        
        [tableView].forEach { v in
            v.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(v)
        }
        
        let g = view.safeAreaLayoutGuide
        
        NSLayoutConstraint.activate([
            
            tableView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
            tableView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
            tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
            tableView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -40.0),
            
        ])
        
        tableView.register(DynamicHeightCell.self, forCellReuseIdentifier: "c")
        tableView.dataSource = self
        tableView.delegate = self
        
        // so we can see the tableView frame
        tableView.backgroundColor = .lightGray
        
        tableView.contentInset.top = 150
        tableView.contentOffset.y = -150
        
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return testRowCount
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let c = tableView.dequeueReusableCell(withIdentifier: "c", for: indexPath) as! DynamicHeightCell
        
        // we want dynamic height cells, so
        var s = "Row: \(indexPath.row   1)"
        
        // to make it easy to see it's the last row
        if indexPath.row == tableView.numberOfRows(inSection: 0) - 1 {
            s  = " --- Last Row"
        }
        
        // fill cells with 1 to 4 rows of text
        for i in 0..<(indexPath.row % 4) {
            s  = "\nThis is Line \(i   2)"
        }

        c.theLabel.text = s

        // cycle background color to make it easy to see the cell frames
        c.contentView.backgroundColor = bkgColors[indexPath.row % bkgColors.count]
        
        return c
    }
    
}

It looks like this when run:

enter image description here

So far, though, it's in your current condition -- we can't scroll up to the top.

What we need to do is find a way to set the table view's .contentInset.bottom:

enter image description here

So, we'll implement scrollViewDidScroll(...):

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    
    // unwrap optional
    if let rows = tableView.indexPathsForVisibleRows {
        
        // get indexPath of final cell
        let n = tableView.numberOfRows(inSection: 0)
        let lastRowIndexPath = IndexPath(row: n - 1, section: 0)
        
        // if final cell is visible
        if rows.contains(lastRowIndexPath) {
            
            // we now know the tableView's contentSize, so
            //  if .contentSize.height is less than tableView.frame.height
            if tableView.contentSize.height < tableView.frame.height {
                // calculate and set bottom inset
                tableView.contentInset.bottom = tableView.frame.height - tableView.contentSize.height
            }
            
        }
        
    }
    
}

Now when we run that, we can "scroll to the top":

enter image description here

Change the testRowCount at the top of the controller from 5 to 6, 7, 8, 20, 30, etc. Once there are enough rows (or the rows are taller) so the table view can scroll to the top without the .contentInset.bottom we get "normal" scrolling while maintaining the 150-point top inset.

Worth noting: the above scrollViewDidScroll code will end up running every time the table is scrolled. Ideally, we would only let it run until we've determined the bottom offset (if one is needed).

To do that, we need a couple new var properties and some if testing.

Here's another version of that controller that stops testing once we know what's needed:

class TableInsetVC: UIViewController, UITableViewDelegate, UITableViewDataSource {

    // we'll use this for both the .contentInset.bottom
    //  AND to stop testing the height when we've determined whether it's needed or not
    var bottomInset: CGFloat = -1

    // we don't want to start testing the height until AFTER initial layout has finished
    //  so we'll use this as a flag
    var hasAppeared: Bool = false

    // set the number of rows to use
    //  once we get past 7 (or so), the rows will be
    //  taller than the tableView frame
    let testRowCount = 5

    let tableView: UITableView = {
        let v = UITableView()
        return v
    }()
    
    // we'll cycle through colors for the cell backgrounds
    //  to make it easier to see the cell frames
    let bkgColors: [UIColor] = [
        .systemRed, .systemGreen, .systemBlue, .systemYellow, .systemCyan, .systemBrown,
    ]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemBackground
        
        [tableView].forEach { v in
            v.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(v)
        }
        
        let g = view.safeAreaLayoutGuide
        
        NSLayoutConstraint.activate([
            
            tableView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
            tableView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
            tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
            tableView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -40.0),
            
        ])
        
        tableView.register(DynamicHeightCell.self, forCellReuseIdentifier: "c")
        tableView.dataSource = self
        tableView.delegate = self
        
        // so we can see the tableView frame
        tableView.backgroundColor = .lightGray
        
        tableView.contentInset.top = 150
        tableView.contentOffset.y = -150
        
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        hasAppeared = true
    }
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {

        // initial layout has finished, AND we have not yet changed bottomInset
        if hasAppeared, bottomInset == -1 {

            // unwrap optional
            if let rows = tableView.indexPathsForVisibleRows {

                // get indexPath of final cell
                let n = tableView.numberOfRows(inSection: 0)
                let lastRowIndexPath = IndexPath(row: n - 1, section: 0)
                
                // if final cell is visible
                if rows.contains(lastRowIndexPath) {

                    // we now know the tableView's contentSize, so
                    //  if .contentSize.height is less than tableView.frame.height
                    if tableView.contentSize.height < tableView.frame.height {
                        // calculate and set bottom inset
                        bottomInset = tableView.frame.height - tableView.contentSize.height
                        tableView.contentInset.bottom = bottomInset
                    } else {
                        // .contentSize.height is greater than tableView.frame.height
                        //  so we don't set .contentInset.bottom
                        //  and we set bottomInset to -2 so we stop testing
                        bottomInset = -2
                    }
                    
                } else {
                    
                    // final cell is not visible, so
                    // if we have scrolled up past the top,
                    //  we know the full table is taller than the tableView
                    //  and we set bottomInset to -2 so we stop testing
                    if tableView.contentOffset.y >= 0 {
                        bottomInset = -2
                    }
                    
                }
            }
            
        }

    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return testRowCount
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let c = tableView.dequeueReusableCell(withIdentifier: "c", for: indexPath) as! DynamicHeightCell
        
        // we want dynamic height cells, so
        var s = "Row: \(indexPath.row   1)"
        
        // to make it easy to see it's the last row
        if indexPath.row == tableView.numberOfRows(inSection: 0) - 1 {
            s  = " --- Last Row"
        }
        
        // fill cells with 1 to 4 rows of text
        for i in 0..<(indexPath.row % 4) {
            s  = "\nThis is Line \(i   2)"
        }
        
        c.theLabel.text = s
        
        // cycle background color to make it easy to see the cell frames
        c.contentView.backgroundColor = bkgColors[indexPath.row % bkgColors.count]

        return c
    }
    
}
  • Related