调整UITableView以适应内容大小

216

我正在创建一个应用程序,其中会在UILabel中提出问题,并在UITableView中显示多个选择答案,每一行显示一个选择。 由于问题和答案各不相同,因此我需要这个UITableView的高度是动态的。

我希望找到一个sizeToFit的解决方法,使表格的框架设置为其所有内容的高度。

有人能指导我如何实现吗?


对于想在OSX上执行类似操作的人,请参见此问题:https://dev59.com/62XWa4cB1Zd3GeqPPrUO - Michael Teper
1
@MiMo你好,能否解释一下"sizeToFit"方法有什么问题吗? :) - iamdavidlam
我想找到一个“sizeToFit”的解决方法。那么你是否尝试过UIView的-sizeToFit方法? - Slipp D. Thompson
我可以问与此相关的问题吗?我已经使用了您的方法,但是没有将父级TableView高度设置为子级TableView内容大小。 - Tekhe
25个回答

191

不需要使用KVO、DispatchQueue或手动设置约束的Swift 5和4.2解决方案。

这个解决方案基于Gulz的回答

  1. 创建一个UITableView的子类:

    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 + adjustedContentInset.top)
        }
    }
    
  2. UITableView添加到您的布局中,并在所有边上设置约束。将底部约束关系设置为>=(大于等于)。

  3. 将其自定义类设置为ContentSizedTableView

  4. 您可能会看到一些错误,因为Storyboard没有考虑我们子类的intrinsicContentSize。通过打开大小检查器并将intrinsicContentSize覆盖为占位符值来修复此问题。这是设计时的覆盖。在运行时,它将使用我们ContentSizedTableView类中的覆盖。


更新:更改了适用于Swift 4.2的代码。如果您使用的是早期版本,请使用UIViewNoIntrinsicMetric代替UIView.noIntrinsicMetric


2
非常好的答案,对我很有帮助,谢谢。不过有一件事 - 在我的情况下,我需要在Storyboard中将tableView的类更改为IntrinsicTableView。我不需要将我的表视图嵌入到另一个UIView中。 - dvp.petrov
2
Swift 4 这个对我来说完全没问题 tableView.sizeToFit() - Soufiane ROCHDI
是的,如果你的内容是静态的,那可能是一个不错的解决方案。但是,使用我的方法,您可以使用自动布局将具有动态内容的tableView嵌入其他视图中,并始终保持其完整高度,而无需考虑在哪些位置需要调用sizeToFit() - heyfrank
3
很棒,谢谢!最后为表格视图匹配父级并包装内容... - Amber K
1
两件事情:1)确保将底部约束设置为>=(greaterThanOrEqual) 2)如果您正在为空表消息设置backgroundView,请使用以下代码替换intrinsicContentSizeoverride var intrinsicContentSize: CGSize { layoutIfNeeded() if contentSize.height > 0 { return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height) } else { return super.intrinsicContentSize } } - cuddergambino
显示剩余16条评论

170

实际上,我自己找到了答案。

我只是为 tableView.frame 创建一个新的 CGRect ,其高度为 table.contentSize.height

这将设置 UITableView 的高度为其内容的高度。 由于代码修改了UI,请不要忘记在主线程中运行它:

dispatch_async(dispatch_get_main_queue(), ^{
        //This code will run in the main thread:
        CGRect frame = self.tableView.frame;
        frame.size.height = self.tableView.contentSize.height;
        self.tableView.frame = frame;
    });

3
使用这种方法可能会遇到问题。如果你有一个较大的列表,那么框架的高度可能会截断一些单元格。如果启用了弹跳功能并滚动到底部,可以看到一些单元格跳出视图。 - whyoz
33
这段代码最好放在哪里?我尝试将其放在viewDidLoad中,但它似乎没有起作用,可能是因为tableview代理方法还没有被触发。什么代理方法最合适? - Kyle Clegg
6
我在viewDidAppear中完成了这个操作,看起来它似乎有效。请注意,表格视图可能比其内容稍微高一些,因为它会留出一些空间用于一个区域的页眉和页脚。 - Jameo
问题是,如果我有一个动态表格,那么在哪里放置这段代码呢?这意味着我有一个可点击的部分视图,并且它在单击时显示或隐藏其行。 - user2083364
2
我发现viewDidAppear是一个很好的地方,可以放置在所有初始tableView cellForRowAtIndexPath调用完成后想要发生的事情。 - rigdonmr
显示剩余5条评论

122

Swift 解决方案

按照以下步骤进行操作:

  1. 在 storyboard 中为表格设置高度约束。

  2. 从 storyboard 拖拽高度约束并在视图控制器文件中创建 @IBOutlet

@IBOutlet var tableHeight: NSLayoutConstraint!
  • 接下来你可以使用以下代码动态地改变表格的高度:

    override func viewWillLayoutSubviews() {
        super.updateViewConstraints()
        self.tableHeight?.constant = self.table.contentSize.height
    }
    
  • 如果最后一行被截断,请尝试在willDisplay cell函数中调用viewWillLayoutSubviews()

    func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        self.viewWillLayoutSubviews()
    }
    

    7
    这个解决方案在我的 Xcode 8.3 上总是会切掉最后一行。 - Jen C
    @Jec C:对我来说也不行。 - KMC
    2
    我必须将这个放在cellForRowAtIndexPath:方法中。如果我将其放在viewWillLayoutSubviews方法中,计算出的contentSize高度是基于估计高度而不是实际内容高度的。 - Manu Mateos
    完美的答案。 - Mehul
    完美解决方案 - Kiran S Kulkarni
    显示剩余3条评论

    22

    我在 iOS 7 上尝试过这个方法,它对我有效。

    - (void)viewDidLoad
    {
        [super viewDidLoad];
        [self.tableView sizeToFit];
    }
    

    3
    如果您的UITableView是动态的(意味着信息在运行时从其中加载和卸载),则需要将此代码放在其他位置,而不是viewDidLoad方法中。我将其放在了cellForRowAtIndexPath方法中,并且还需要将"tableView"更改为我的IBOutlet属性名称,以避免旧的“找不到tableView属性”的错误。 - jeremytripp
    谢谢,我在调整弹出窗口内的表格视图大小时遇到了问题,但现在已经解决了。 - Zaporozhchenko Oleksandr

    19

    为表视图的contentSize属性添加观察者,并相应地调整框架大小。

    [your_tableview addObserver:self forKeyPath:@"contentSize" options:0 context:NULL];
    

    然后在回调函数中:

    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
        {
             CGRect frame = your_tableview.frame;
             frame.size = your_tableview.contentSize;
             your_tableview.frame = frame;
        }
    

    希望这可以帮助你。


    2
    你应该再添加一件事情。你应该移除表视图的观察者。因为如果单元格被释放,它会崩溃。 - Mahesh Agrawal

    18

    我在滚动视图内部放置了一个表格视图并需要计算表格视图的高度并相应地调整大小。以下是我采取的步骤:

    0)向你的滚动视图添加一个UIView(可能不需要这一步,但我这样做是为了避免任何可能的冲突)-这将是你的表格视图的容器视图。如果你采取了这个步骤,则将该视图的边框设置为tableview的边框。

    1)创建一个UITableView的子类:

    class IntrinsicTableView: UITableView {
    
        override var contentSize:CGSize {
            didSet {
                self.invalidateIntrinsicContentSize()
            }
        }
    
        override var intrinsicContentSize: CGSize {
            self.layoutIfNeeded()
            return CGSize(width: UIViewNoIntrinsicMetric, height: contentSize.height)
        }
    
    }
    

    2)在Storyboard中将表视图的类设置为IntrinsicTableView:截图:http://joxi.ru/a2XEENpsyBWq0A

    3)将高度约束设置为您的表视图

    4)将表格的IBoutlet拖到您的ViewController上

    5)将表格高度约束的IBoutlet拖到您的ViewController上

    6)将此方法添加到您的ViewController中:

    override func viewWillLayoutSubviews() {
            super.updateViewConstraints()
            self.yourTableViewsHeightConstraint?.constant = self.yourTableView.intrinsicContentSize.height
        }
    
    希望这可以帮到你。

    请查看我的答案,它基于您的子类,但不需要设置高度约束。 - heyfrank
    @fl034 提供链接? - Gulz
    @fl034 我认为当你的元素嵌入到ScrollView中时,你无法避免使用约束。我的情况就是与ScrollView有关。 - Gulz
    我也在滚动视图中放置了我的表格视图,它完美地工作着 :) - heyfrank
    在iOS 13/Xcode 11上对我有效。您终止了3天的痛苦,非常感谢您。 - Mehdi S.

    12

    Swift 5 解决方案

    按照以下四个步骤进行操作:

    1. 从故事板中设置表格视图的高度约束。

    2. 从故事板中拖动高度约束,并在视图控制器文件中创建一个 @IBOutlet 引用它。

    3. @IBOutlet var tableViewHeightConstraint: NSLayoutConstraint!
      
    4. override func viewDidLoad() 中为 contentSize 属性添加观察者。

    override func viewDidLoad() {
            super.viewDidLoad()
            self.tableView.addObserver(self, forKeyPath: "contentSize", options: .new, context: nil)
     
        }
    
    
  • 接着,您可以使用以下代码动态更改表的高度:

  • override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
         if(keyPath == "contentSize"){
             if let newvalue = change?[.newKey]
             {
                 DispatchQueue.main.async {
                 let newsize  = newvalue as! CGSize
                 self.tableViewHeightConstraint.constant = newsize.height
                 }
    
             }
         }
     }
    

    8

    如果您不想自己跟踪表格视图的内容大小更改,您可能会发现这个子类很有用。

    protocol ContentFittingTableViewDelegate: UITableViewDelegate {
        func tableViewDidUpdateContentSize(_ tableView: UITableView)
    }
    
    class ContentFittingTableView: UITableView {
    
        override var contentSize: CGSize {
            didSet {
                if !constraints.isEmpty {
                    invalidateIntrinsicContentSize()
                } else {
                    sizeToFit()
                }
    
                if contentSize != oldValue {
                    if let delegate = delegate as? ContentFittingTableViewDelegate {
                        delegate.tableViewDidUpdateContentSize(self)
                    }
                }
            }
        }
    
        override var intrinsicContentSize: CGSize {
            return contentSize
        }
    
        override func sizeThatFits(_ size: CGSize) -> CGSize {
            return contentSize
        }
    }
    

    1
    对于Swift 3,intrinsicContentSize已经变成了一个var,就像override public var intrinsicContentSize: CGSize { //... return someCGSize } - xaphod
    1
    对我来说不起作用。仍然隐藏表格底部。 - Gulz

    7

    7
    我用了一种稍微不同的方法,实际上我的TableView是在ScrollView内部,所以我必须给它一个高度约束为0。
    然后在运行时,我进行了以下更改,
    func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        self.viewWillLayoutSubviews()
    }
    
    override func viewWillLayoutSubviews() {
        super.updateViewConstraints()
        DispatchQueue.main.async {
            self.tableViewHeightConstraint?.constant = self.myTableView.contentSize.height
            self.view.layoutIfNeeded()
        }
    }
    

    1
    如果您的TableView在ScrollView中,那么这是完美的答案。 - Threadripper

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接