如何让 tableFooterView 始终位于 UITableView 的底部

Posted

技术标签:

【中文标题】如何让 tableFooterView 始终位于 UITableView 的底部【英文标题】:How to keep tableFooterView always on bottom of UITableView 【发布时间】:2019-05-14 14:20:38 【问题描述】:

我有一个UITableView,其中包含可变数量的部分。每个部分都有可变数量的单元格,每个部分都有页眉和页脚。我的UITableView 也有一个tableFooterView,我想一直保持在屏幕底部,除非表格太大而无法放在屏幕上,那么tableFooterView 应该显示在最后一部分下方.这里说明了我想要完成的事情:

Example of what I want, scenario 1

Example of what I want, scenario 2

但是,目前tableFooterView 总是位于最后一个部分的正下方,因此例如只有两个部分时,它看起来像这样:

Example of what I currently have

我正在寻找一种方法,使其在所有可能的情况下始终保持在底部。我一直在环顾四周,因为 Apple 不支持 tableFooterView 的 AutoLayout,所以我还没有找到解决方案。类似的情况在最后一部分用sectionFooter 替换tableFooterView,但我不能这样做,因为我已经有sectionFooters

有没有人可以帮助我或为我指明正确的方向?有几点需要考虑:

必须是tableFooterView; 用户可以将部分添加到UITableView 并将行添加到部分,因此 tableFooterView 应随后更新其位置

我现在如何设置tableFooterView

class CustomView: UITableViewDelegate, UITableViewDataSource 

    var myTableFooter: UIView = 

        let myTableFooter = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 50))
        myTableFooter.backgroundColor = .red
        myTableFooter.isUserInteractionEnabled = true
        return myTableFooter

    ()

    override init(frame: CGRect) 

        super.init(frame: frame)
        setupViews()

        MyTableView.tableFooterView = myTableFooter

    


编辑:按照建议尝试了scrollViewDidScroll 方法,但没有奏效:

func scrollViewDidScroll(_ scrollView: UIScrollView) 

    if(scrollView == myTableView) 

        let neededHeight = myTableView.frame.height - 50 - view.safeAreaInsets.bottom
        let currentHeight = myTableView.contentSize.height - 50

        let heightDifference = neededHeight - currentHeight

        if(heightDifference > 0) 

            myTableView.tableFooterView?.transform = CGAffineTransform(translationX: 0, y: heightDifference)

        

    


【问题讨论】:

您是否考虑过切换到 UICollectionView 并进行自定义布局? 这是一个选择。但是我已经有一个 UICollectionView 作为我的主视图,UICollectionView 中有两个单元格,每个单元格都包含一个 UITableView。我相信应该有一种方法可以用 UITableView 完成我想要的。 First...“Apple 不支持 tableFooterView 的 AutoLayout” --- 是和否。它需要额外的一两个步骤,但您当然可以在 tableFooterView 中使用 AutoLayout ... 但是,这可能无法解决您的问题(例如,您不能在其他地方限制页脚视图)。一种选择是使用嵌入在滚动视图中的“自动调整大小”tableView,同时在滚动视图中嵌入标准的“footerView”UIView 在tableview下面添加一个视图 我很乐意,@RajeshKumarR。但我不知道当 UITableView 太大而无法在屏幕上显示时它会如何跟随它的内容。有示例代码吗? 【参考方案1】:

一种方法是:

使用扩展来定义“自动调整大小的非滚动”表格视图 在“容器”视图中嵌入表格视图和“页脚”视图的普通UIView 将容器视图嵌入到滚动视图中,高度与滚动视图相同,但优先级较低 将页脚视图限制在容器视图的底部,将>= 限制在表格视图的底部

所以,tableView的“auto-height”+footer view的高度决定了container view的高度,也就决定了scroll view的.contentSize。页脚视图将“粘”到容器视图的底部。当滚动视图有足够的内容时,它会“下推”页脚视图。

例子:

这是创建它的代码。一切都通过代码完成...不需要 IBOutlets,所以只需创建一个新的视图控制器并将其类分配给 PennyWiseViewController

//
//  PennyWiseViewController.swift
//
//  Created by Don Mag on 5/14/19.
//

import UIKit

final class ContentSizedTableView: UITableView 

    override var contentSize:CGSize 
        didSet 
            invalidateIntrinsicContentSize()
        
    

    override var intrinsicContentSize: CGSize 
        layoutIfNeeded()
        return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
    



class MyOneLabelCell: UITableViewCell 

    // very simple one-label tableView cell

    let theLabel: UILabel = 
        let v = UILabel()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.numberOfLines = 0
        return v
    ()

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) 
        super.init(style: style, reuseIdentifier: reuseIdentifier)

        contentView.addSubview(theLabel)

        NSLayoutConstraint.activate([
            theLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8.0),
            theLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8.0),
            theLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8.0),
            theLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8.0),
            ])

    

    required init?(coder aDecoder: NSCoder) 
        fatalError("init(coder:) has not been implemented")
    



class PennyWiseViewController: UIViewController, UITableViewDelegate, UITableViewDataSource 

    let theContainerView: UIView = 
        let v = UIView()
        v.translatesAutoresizingMaskIntoConstraints = false
        return v
    ()

    let theScrollView: UIScrollView = 
        let v = UIScrollView()
        v.translatesAutoresizingMaskIntoConstraints = false
        return v
    ()

    let theTableView: ContentSizedTableView = 
        let v = ContentSizedTableView()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.isScrollEnabled = false
        return v
    ()

    let theFooterView: UILabel = 
        let v = UILabel()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.backgroundColor = .red
        v.textColor = .white
        v.text = "The Footer View"
        v.textAlignment = .center
        return v
    ()

    // start with 3 sections
    // selecting the row in the first section allows adding sections
    // selecting the row in the second section allows deleting sections
    var numSections = 3

    let reuseID = "MyOneLabelCell"

    override func viewDidLoad() 
        super.viewDidLoad()

        theTableView.dataSource = self
        theTableView.delegate = self

        theTableView.register(MyOneLabelCell.self, forCellReuseIdentifier: reuseID)

        // add the views
        view.addSubview(theScrollView)
        theScrollView.addSubview(theContainerView)
        theContainerView.addSubview(theTableView)
        theContainerView.addSubview(theFooterView)

        // this will allow the container height to be at least the height of the scroll view
        // when enough content is added to the container, it will grow
        let containerHeightConstraint = theContainerView.heightAnchor.constraint(equalTo: theScrollView.heightAnchor, multiplier: 1.0)
        containerHeightConstraint.priority = .defaultLow

        NSLayoutConstraint.activate([

            // constrain scrollView to all 4 sides (safe-area)
            theScrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            theScrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
            theScrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
            theScrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),

            // constrain containerView to all 4 sides of scrollView
            theContainerView.topAnchor.constraint(equalTo: theScrollView.topAnchor),
            theContainerView.bottomAnchor.constraint(equalTo: theScrollView.bottomAnchor),
            theContainerView.leadingAnchor.constraint(equalTo: theScrollView.leadingAnchor),
            theContainerView.trailingAnchor.constraint(equalTo: theScrollView.trailingAnchor),

            theContainerView.widthAnchor.constraint(equalTo: theScrollView.widthAnchor),

            // constrain tableView to top/leading/trailing of constainerView
            theTableView.topAnchor.constraint(equalTo: theContainerView.topAnchor),
            theTableView.leadingAnchor.constraint(equalTo: theContainerView.leadingAnchor),
            theTableView.trailingAnchor.constraint(equalTo: theContainerView.trailingAnchor),

            // constrain footerView >= 20 from bottom of tableView
            theFooterView.topAnchor.constraint(greaterThanOrEqualTo: theTableView.bottomAnchor, constant: 20.0),

            theFooterView.leadingAnchor.constraint(equalTo: theContainerView.leadingAnchor, constant: 0.0),
            theFooterView.trailingAnchor.constraint(equalTo: theContainerView.trailingAnchor, constant: 0.0),
            theFooterView.bottomAnchor.constraint(equalTo: theContainerView.bottomAnchor, constant: 0.0),

            theFooterView.heightAnchor.constraint(equalToConstant: 150.0),

            containerHeightConstraint,

            ])

    


    // MARK: - Table view data source

    func numberOfSections(in tableView: UITableView) -> Int 
        return numSections
    

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int 
        return section < 2 ? 1 : 2
    

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

        switch indexPath.section 
        case 0:
            cell.theLabel.text = "Add a section"
        case 1:
            cell.theLabel.text = "Delete a section"
        default:
            cell.theLabel.text = "\(indexPath)"
        

        return cell
    

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) 
        tableView.deselectRow(at: indexPath, animated: true)

        switch indexPath.section 
        case 0:
            numSections += 1
            tableView.reloadData()
        case 1:
            if numSections > 2 
                numSections -= 1
                tableView.reloadData()
            
        default:
            print("\(indexPath) was selected")
        

    

    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? 
        return "Section \(section) Header"
    

    func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? 
        return "Section \(section) Footer"
    


【讨论】:

哇!这看起来很有希望。晚饭后我会研究这个,非常感谢你到目前为止所做的努力。我会告诉你进展如何。 嗨伙计,我目前正在我现有的项目中实现这个,它看起来很有希望。我仍然需要做一些改变,但我相信这应该会奏效。但是,当我插入新行或删除行时,我使用持续时间为 0.25 秒的动画。由于内容大小无效,动画看起来不太好。知道如何解决这个问题吗? 所以我目前正在玩这个解决方案。到目前为止,我还没有找到正确动画行/节插入或删除的方法。在 TableView 本身上插入似乎很好,而删除行/部分会使内容“跳跃”。看起来 contentSize 是在动画完成之前设置的,所以动画有点消失了。 TableFooterView 也没有动画,只是移动(落)到它应该“结束”的地方。知道这是否可以解决? @PennyWise - tableView 可能不是要走的路......因为您希望表格视图垂直扩展以适应行数,所以您没有重用单元格,所以您可以简单地使用自定义UIView(而不是UITableViewCell)并操纵约束——或者将它们放在堆栈视图中。 我绝对需要一个 UITableView。现在,我在添加或删除单元格或部分(方法 insertSections、deleteSections、insertRows、deleteRows)时删除了所有动画,它可以根据需要工作。我唯一不知道的是如何在更新intrinsicContentSize 时执行动画。我可能暂时不使用动画。也尝试过使用 heightAnchor 约束但没有成功。【参考方案2】:

您可以通过在滚动表格时手动平移页脚视图的框架来做到这一点。您需要执行以下操作:

    将视图设置为tableFooterView。 响应UIScrollViewDelegate的scrollViewDidScroll方法。 计算偏移页脚视图的量并将其设置为转换:tableView.tableFooterView?.transform = CGAffineTransform(translationX: 0, y: &lt;some value&gt;)

【讨论】:

试过了,但无法正常工作。框架没有更新,但所有检查都通过了(做了一些打印来测试是否存在某种错误)。我将在 OP 中添加我的代码以供参考。【参考方案3】:

我在 Github 做了一个演示:StickTableFooterView

如何让tableFooterView 始终在UITableView 的底部?

    将视图创建为 tableFooterView。 创建一个视图并将其作为子视图(Pretended tableFooterView)放入 tableFooterView。 设置适当的布局约束。 享受您的 tableFooterView 在 UITableView 底部的滴答声。
由于伪装的tableFooterView超出了其superview的范围,需要处理touch事件。请参阅 ViewWithOutb​​oundsButtons.m。

关于演示

红色区域指的是 tableFooterView。 黄色区域指的是伪装的 tableFooterView。

-----更新------

设置适当的布局约束:

innerView.translatesAutoresizingMaskIntoConstraints = false;
[NSLayoutConstraint constraintWithItem:innerView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:innerView.superview attribute:NSLayoutAttributeRight multiplier:1 constant:0].active = YES;
[NSLayoutConstraint constraintWithItem:innerView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:innerView.superview attribute:NSLayoutAttributeLeft multiplier:1 constant:0].active = YES;
[NSLayoutConstraint constraintWithItem:innerView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:innerView.superview attribute:NSLayoutAttributeBottom multiplier:1 constant:0].active = YES;
[NSLayoutConstraint constraintWithItem:innerView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:self.tableView.superview attribute:NSLayoutAttributeBottom multiplier:1 constant:0].active = YES;
[NSLayoutConstraint constraintWithItem:innerView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:innerViewHeight].active = YES;

关键是用tableView.superview设置布局约束:

[NSLayoutConstraint constraintWithItem:innerView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:self.tableView.superview attribute:NSLayoutAttributeBottom multiplier:1 constant:0].active = YES;

【讨论】:

不是链接到存储库,您能否将相关代码粘贴到您的答案中并解释它是如何解决问题的? @C.Peck,当然,添加一些解释。

以上是关于如何让 tableFooterView 始终位于 UITableView 的底部的主要内容,如果未能解决你的问题,请参考以下文章

网页CSS定位问题 ,如何让块bottom始终位于页面最下方了?

在 iOS 中,如何创建一个始终位于所有其他视图控制器之上的按钮?

如何使页脚视图始终位于 UITableViewController 的底部?

页脚始终位于底部但不固定

如何使用 flexbox 让中间元素始终完美居中 [重复]

如何将高度设置为从 xib 加载的 tableFooterView