我啥时候应该重新计算 UITableView 页脚的大小?

Posted

技术标签:

【中文标题】我啥时候应该重新计算 UITableView 页脚的大小?【英文标题】:When Should I Recalculate A UITableView Footer's Size?我什么时候应该重新计算 UITableView 页脚的大小? 【发布时间】:2018-10-04 13:32:55 【问题描述】:

我想根据表格视图不断变化的内容大小重新计算表格视图页脚的高度。当表格的行数为零时,页脚的高度将达到最大值。随着行被添加到表格中,页脚的高度将减少,直到达到最小值。我正在做的是使用页脚填充零或几行时出现在表格底部的空白空间。除了添加的行之外,内容大小也可能会发生变化,因为现有行的高度(内容)已更改。

假设我有一个视图控制器,它的主视图包含两个子视图:一个按钮和一个表格视图。单击该按钮会导致修改数据存储并调用表的 reloadData 方法。何时/在何处为表的 tableFooterView.bounds.size.height 分配一个新值?

我还应该指出我正在使用 UITableViewAutomaticDimension。如果,在表格的数据源委托方法 cellForRowAt 中,我打印得到的单元格高度:

Upper table cell height = 21.0
Upper table cell height = 21.0
Upper table cell height = 21.0
Upper table cell height = 21.0
Upper table cell height = 44.0

所有 21 个,除了最后一个,即新的。这一定是由于尚未应用自动尺寸标注。

更新:

我初步得出了以下解决方案(非常感谢this 线程上的所有人提供了解决方案的最大部分)。我是暂定的,因为解决方案涉及调用 reloadData 两次以处理 contentSize 的问题。有关 contentSize 问题的演示,请参阅 this GitHub 项目。

class TableView: UITableView 

    override func reloadData() 
        execute()  super.reloadData() 
    

    override func reloadRows(at indexPaths: [IndexPath], with animation: UITableView.RowAnimation) 
        execute()  super.reloadRows(at: indexPaths, with: animation) 
    

    private func execute(reload: @escaping () -> Void) 
        CATransaction.begin()
        CATransaction.setCompletionBlock() 
            if self.adjustFooter() 
                reload() // Cause the contentSize to update (see GitHub project)
                self.layoutIfNeeded()
            
        
        reload()
        CATransaction.commit()
    

    // Return true(false) if the footer was(was not) adjusted
    func adjustFooter() -> Bool 
        guard let currentFrame = tableFooterView?.frame else  return false 

        let newHeight = calcFooterHeight()
        let adjustmentNeeded = newHeight != currentFrame.height

        if adjustmentNeeded 
            tableFooterView?.frame = CGRect(x: currentFrame.minX, y: currentFrame.minY, width: currentFrame.width, height: newHeight)
        

        return adjustmentNeeded
    

    private let minFooterHeight: CGFloat = 44
    private func calcFooterHeight() -> CGFloat 
        guard let footerView = tableFooterView else  return 0 

        let spaceTaken = contentSize.height - footerView.bounds.height
        let spaceAvailable = bounds.height - spaceTaken
        return spaceAvailable > minFooterHeight ? spaceAvailable : minFooterHeight
    

【问题讨论】:

您确定需要手动计算页脚视图的大小吗?如果您使用UITableViewtableFooterView 属性,页脚视图将自动调整大小。 @matt 是的,我是发起变革的人。但是,尽我所能确定,我需要等到表格视图更新后才能执行任何计算 - 即在我知道行高之前。我可以排队一些未来的工作,但感觉很笨拙。 @AlexSmet 你确定吗?根据我的经验,tableFooterView 的大小会随着行添加到表中而更新。 我错了,对不起。但我可以提供一些解决方案,请看我的回答。 @matt 我非常重视您对我的暂定解决方案的反馈。 【参考方案1】:

UITableViewDelegatetableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat 方法,我们可以使用它来指定节页脚的高度。当我们为表格视图调用reloadData() 或更改屏幕方向等时会触发此方法。

所以你可以实现这个方法来计算一个新的页脚高度:

    override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat 

    guard section == 0 else  return 0.0  // assume there is only one section in the table

    var cellsHeight: CGFloat = 0.0

    let rows = self.tableView(tableView, numberOfRowsInSection: section)

    for row in 0..<rows
    
        let indexPath = IndexPath(item: row, section: section)
        cellsHeight += self.tableView(tableView, heightForRowAt: indexPath)
    

    let headerHeight: CGFloat = tableView.tableHeaderView?.frame.height ?? 0.0
    let footerHeight = view.frame.height - headerHeight - cellsHeight

    return footerHeight

【讨论】:

谢谢。 tableFooterView 不是节页脚,对吧?实际上,由于我的表格只有 1 个部分,我正在考虑放弃表格页脚并改用部分页脚。您确定在调用 heightForFooterInSection 时行高已经完成(回想一下我正在使用 UITableViewAutomaticDimension)吗?【参考方案2】:

我得出了以下解决方案。非常感谢this 线程上的所有人提供解决方案的最大部分。 TableViewController.TableView 类提供了所需的功能。代码的其余部分充实了一个完整的示例。

//
//  TableViewController.swift
//  Tables
//
//  Created by Robert Vaessen on 11/6/18.
//  Copyright © 2018 Robert Vaessen. All rights reserved.
//
//  Note: Add the following to AppDelegate:
//
//    func application(_ application: UIApplication,
//                    didFinishLaunchingWithOptions launchOptions: 
//                    [UIApplication.LaunchOptionsKey: Any]?) -> Bool 
//        window = UIWindow(frame: UIScreen.main.bounds)
//        window?.makeKeyAndVisible()
//        window?.rootViewController = TableViewController()
//        return true
//    


import UIKit

class TableViewController: UIViewController 

    class TableView : UITableView 

        override func reloadData() 
            execute()  super.reloadData() 
        

        override func reloadRows(at indexPaths: [IndexPath], with animation: UITableView.RowAnimation) 
            execute()  super.reloadRows(at: indexPaths, with: animation) 
        

        private func execute(reload: @escaping () -> Void) 
            CATransaction.begin()
            CATransaction.setCompletionBlock() 
                print("Reload completed")
                _ = self.adjustFooter()
            
            print("\nReload begun")
            reload()
            CATransaction.commit()
        

        private func adjustFooter() -> Bool 
            guard let footerView = tableFooterView else  return false 

            func calcFooterHeight() -> CGFloat 
                var heightUsed = tableHeaderView?.bounds.height ?? 0
                for cell in visibleCells  heightUsed += cell.bounds.height 
                let heightRemaining = bounds.height - heightUsed

                let minHeight: CGFloat = 44
                return heightRemaining > minHeight ? heightRemaining : minHeight
            

            let newHeight = calcFooterHeight()
            guard newHeight != footerView.bounds.height else  return false 

            // Keep the origin where it is, i.e. tweaking just the height expands the frame about its center
            let currentFrame = footerView.frame
            footerView.frame = CGRect(x: currentFrame.origin.x, y: currentFrame.origin.y, width: currentFrame.width, height: newHeight)

            return true
        
    

    class FooterView : UIView 
        override func draw(_ rect: CGRect) 
            print("Drawing footer")
            super.draw(rect)
        
    

    private var tableView: TableView!

    private let cellReuseId = "TableCell"
    private let data: [UIColor] = [UIColor(white: 0.4, alpha: 1), UIColor(white: 0.5, alpha: 1), UIColor(white: 0.6, alpha: 1), UIColor(white: 0.7, alpha: 1)]
    private var dataRepeatCount = 1

    override func viewDidLoad() 
        super.viewDidLoad()

        func createTable(in: UIView) -> TableView 

            let tableView = TableView(frame: CGRect.zero)

            tableView.separatorStyle = .none
            tableView.translatesAutoresizingMaskIntoConstraints = false

            `in`.addSubview(tableView)

            tableView.centerXAnchor.constraint(equalTo: `in`.centerXAnchor).isActive = true
            tableView.centerYAnchor.constraint(equalTo: `in`.centerYAnchor).isActive = true
            tableView.widthAnchor.constraint(equalTo: `in`.widthAnchor, multiplier: 1).isActive = true
            tableView.heightAnchor.constraint(equalTo: `in`.heightAnchor, multiplier: 0.8).isActive = true

            tableView.dataSource = self
            tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellReuseId)

            return tableView
        

        func addHeader(to: UITableView) 
            let header = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 50))
            to.tableHeaderView = header

            let color = UIColor.black
            let offset: CGFloat = 64

            let add = UIButton(type: .system)
            add.setTitle("Add", for: .normal)
            add.layer.borderColor = color.cgColor
            add.layer.borderWidth = 1
            add.layer.cornerRadius = 5
            add.tintColor = color
            add.contentEdgeInsets = UIEdgeInsets.init(top: 8, left: 8, bottom: 8, right: 8)
            add.addTarget(self, action: #selector(addRows), for: .touchUpInside)
            add.translatesAutoresizingMaskIntoConstraints = false
            header.addSubview(add)
            add.centerXAnchor.constraint(equalTo: to.centerXAnchor, constant: -offset).isActive = true
            add.centerYAnchor.constraint(equalTo: header.centerYAnchor).isActive = true

            let remove = UIButton(type: .system)
            remove.setTitle("Remove", for: .normal)
            remove.layer.borderColor = color.cgColor
            remove.layer.borderWidth = 1
            remove.layer.cornerRadius = 5
            remove.tintColor = color
            remove.contentEdgeInsets = UIEdgeInsets.init(top: 8, left: 8, bottom: 8, right: 8)
            remove.addTarget(self, action: #selector(removeRows), for: .touchUpInside)
            remove.translatesAutoresizingMaskIntoConstraints = false
            header.addSubview(remove)
            remove.centerXAnchor.constraint(equalTo: header.centerXAnchor, constant: offset).isActive = true
            remove.centerYAnchor.constraint(equalTo: header.centerYAnchor).isActive = true
        

        func addFooter(to: UITableView) 
            let footer = FooterView(frame: CGRect(x: 0, y: 0, width: 0, height: 50))
            footer.layer.borderWidth = 3
            footer.layer.borderColor = UIColor.red.cgColor
            //footer.contentMode = .redraw
            to.tableFooterView = footer
        

        tableView = createTable(in: view)
        addHeader(to: tableView)
        addFooter(to: tableView)

        view.backgroundColor = .white
        tableView.backgroundColor = .black // UIColor(white: 0.2, alpha: 1)
        tableView.tableHeaderView!.backgroundColor = .cyan // UIColor(white: 0, alpha: 1)
        tableView.tableFooterView!.backgroundColor = .white // UIColor(white: 0, alpha: 1)
    

    @objc private func addRows() 
        dataRepeatCount += 1
        tableView.reloadData()
    

    @objc private func removeRows() 
        dataRepeatCount -= dataRepeatCount > 0 ? 1 : 0
        tableView.reloadData()
    


extension TableViewController : UITableViewDataSource 
    func numberOfSections(in tableView: UITableView) -> Int 
        return 1
    

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int 
        guard section == 0 else  fatalError("Unexpected section: \(section)") 
        return dataRepeatCount * data.count
    

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell 
        let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseId, for: indexPath)

        cell.textLabel?.textAlignment = .center
        cell.backgroundColor = data[indexPath.row % data.count]
        cell.textLabel?.textColor = .white
        cell.textLabel?.text = "\(indexPath.row)"

        return cell
    

【讨论】:

以上是关于我啥时候应该重新计算 UITableView 页脚的大小?的主要内容,如果未能解决你的问题,请参考以下文章

我啥时候应该增加 xcdatamodeld 版本?

我啥时候应该使用“while 循环”?

如何在 UITableView swift 3 中使用动画在底部滚动

在不重新加载整个表格视图的情况下更改 UITableView 的部分页眉/页脚标题

我啥时候应该使用 FutureBuilder?

刷新 UITableView tableFooterView 而不重新加载整个表数据