5

We have a portion of our UI which is a small list of labels with color swatches next to them. The design I'm taking over has six of these hard-coded in the layout even though the actual data is dynamic, meaning if we only need to show three, we have to explicitly hide three, which also throws off the balance of the page. Making matters worse is each one of those 'items' is actually made up of several sub-views so a screen with six hard-coded items has eighteen IBOutlets.

What I'm trying to do is to instead use a UITableView to represent this small portion of the screen, and since it won't scroll, I was wondering if you can use AutoLayout to configure the intrinsic content height of the UITableView to be based on the number of rows.

Currently I have a test page with a UITableView vertically constrained to the center, but without a height constraint because I am hoping to have the table's intrinsic content size reflect the visible rows. I have also disabled scrolling on the table. When I reload the table, I call updateConstraints. But the table still does not resize.

Note: We can't use a UIStackView (which would have been perfect for this) because we have to target iOS8 and that wasn't introduced until iOS9, hence this solution.

Has anyone been able to do something similar to our needs?

Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286
  • Programmatically can modify the height constraint of table to attain your required height – Janmenjaya Sep 21 '16 at 20:44
  • 1
    This does not sound like a good use of UITableView. It's a subclass of UIScrollView for a reason, and shouldn't have to rely on other cells to determine the height of one. Have you considered a UIStackView? – Connor Neville Sep 21 '16 at 20:47
  • 1
    @ConnorNeville, yes, but with a UIStackView, you have to manually set the constraints every time you're adding or removing data. A UITableView does that for you. And yes, it's a UIScrollView for a reason, but it also has a scrollEnabled property so I have to disagree with your 'not good' assertion or else why would they have added that? It's a perfectly valid use. Take a look at UITextView for the same reason. – Mark A. Donohoe Sep 21 '16 at 20:52
  • @Janmenjaya, yes I could manually set the constraint, but I'm trying to see if the intrinsic content height can be implicitly determined by the number of rows, the same way a UITextView with scrolling disabled has an implicit height, but with it enabled, it doesn't. – Mark A. Donohoe Sep 21 '16 at 20:54
  • You don't have to adjust any constraints when adding or removing things from UIStackView. It sounds to me like a UIStackView with `distribution = FillEqually` or possibly `FillProportionally` is precisely what you want. You could probably get it done your way, but I have never seen a UITableView doing layout logic like this. – Connor Neville Sep 21 '16 at 21:04
  • Sorry. I read that wrong. I thought you said 'ScrollView.' We can't use UIStackView anyway because we're targeting iOS8 and that was introduced in iOS 9. – Mark A. Donohoe Sep 21 '16 at 21:05
  • 2
    Possible duplicate of [Resizing UITableView to fit content](https://stackoverflow.com/questions/2595118/resizing-uitableview-to-fit-content) – heyfrank Feb 05 '18 at 12:51

4 Answers4

7

// Define this puppy:

class AutoTableView: UITableView {   
 
    override func layoutSubviews() {
        super.layoutSubviews()
        self.invalidateIntrinsicContentSize()
    }
    
    override var intrinsicContentSize: CGSize {
        get {
            var height:CGFloat = 0;
            for s in 0..<self.numberOfSections {
                let nRowsSection = self.numberOfRows(inSection: s)
                for r in 0..<nRowsSection {
                    height += self.rectForRow(at: IndexPath(row: r, section: s)).size.height;
                }
            }
            return CGSize(width: UIView.noIntrinsicMetric, height: height)
        }
        set {
        }
    }
}

and make it your class in IB. obs: this is if your class is only cells and shit. if it has header, footer or some other thign, dunno. it'll not work. for my purposes it works

peace

Michal Šrůtek
  • 1,647
  • 16
  • 17
noripcord
  • 3,412
  • 5
  • 29
  • 26
  • 3
    I love this! It's been super helpful. The only change I had to make was to add in the height of header/footer views, so instead of `var height:CGFloat = 0;` my version starts with `var height: CGFloat = (self.tableHeaderView?.frame.height ?? 0) + (self.tableFooterView?.frame.height ?? 0)`. – Lenny T Feb 11 '20 at 16:07
  • can I ask question related to this? I have used your approach but haven't set parent tableview height to child tableview content size – Tekhe Feb 22 '21 at 12:49
  • Just out of curiosity, why are you using `rectForRow` instead of just `heightForRowAtIndexPath`? – Mark A. Donohoe Apr 13 '21 at 16:31
  • there's also ```rect(forSection:)```, so you can just iterate the sections and sum the heights from that rect + header + footer – arolson101 Apr 29 '23 at 04:33
6

Ok, so unlike UITextView, it doesn't look like UITableView ever returns an intrinsic size based on the visible rows. But that's not that big a deal to implement via a subclass, especially if there's a single section, no headers or footers, and the rows are of a fixed height.

class AutoSizingUiTableView : UITableView
{
    override func intrinsicContentSize() -> CGSize
    {
        let requiredHeight = rowCount * rowHeight
        return CGSize(width: UIView.noIntrinsicMetric, height: CGFloat(requiredHeight))
    }
}

I'll leave it up to the reader to figure out how to get their own rowCount. The same if you have variable heights, multiple sections, etc. You just need more logic.

By doing this, it works great with AutoLayout. I just wish it handled this automatically.

Michal Šrůtek
  • 1,647
  • 16
  • 17
Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286
  • No worries, you are quite right, I completely missed your reference to auto layout! But at least it demonstrates proof of concept programmatically - glad you were able to fix your issue. – Marcus Sep 22 '16 at 08:19
  • This doesn't work with dynamic cell heights, does it? See my answer below – heyfrank Apr 13 '21 at 08:24
  • Actually, it does. You just have to call to get each row height either via `rectForRow` or `heightForRowAtindexPath`. That's why I said as much in the 'I'll leave it to you...' section. The point was it does not ever return an intrinsic height and that's the missing piece you need to fill out. By the way, I saw you posted this comment right around when my question was down-voted. Was that also you? If so, mind explaining why? (If not, the disregard of course. Just found it odd on the timing.) – Mark A. Donohoe Apr 13 '21 at 16:33
  • I have been using something similar to this (I am returning the `tableView.contentSize` as the intrinsicContentSize), but I cannot figure out how to get row additions or deletions to be animated correctly. What happens when adding a row is that the frame.size.height is incremented to the new size automatically and then the new row content is added to the table and animated. But how can the table changes be animated correctly? – ashipma May 08 '21 at 04:58
  • Maybe try placing that code inside an animation block. Animation blocks automatically animate resizing of views so that may handle the frame resizing. – Mark A. Donohoe May 08 '21 at 05:01
1

This can be done, please see below for a very simple (and rough - rotation does not work properly!) example, which allows you to update the size of the table view by entering a number in the text field and resetting with a button.

import UIKit

class ViewController: UIViewController {

    var tableViewController : FlexibleTableViewController!
    var textView : UITextView!
    var button : UIButton!
    var count : Int! {
        didSet {
            self.refreshDataSource()
        }
    }
    var dataSource : [Int]!
    let rowHeight : CGFloat = 50

    override func viewDidLoad() {
        super.viewDidLoad()

        // Configure

        self.tableViewController = FlexibleTableViewController(style: UITableViewStyle.plain)

        self.count = 10
        self.tableViewController.tableView.backgroundColor = UIColor.red

        self.textView = UITextView()
        self.textView.textAlignment = NSTextAlignment.center
        self.textView.textColor = UIColor.white
        self.textView.backgroundColor = UIColor.blue

        self.button = UIButton()
        self.button.setTitle("Reset", for: UIControlState.normal)
        self.button.setTitleColor(UIColor.white, for: UIControlState.normal)
        self.button.backgroundColor = UIColor.red
        self.button.addTarget(self, action: #selector(self.updateTable), for: UIControlEvents.touchUpInside)

        self.layoutFrames()

        // Assemble
        self.view.addSubview(self.tableViewController.tableView)
        self.view.addSubview(self.textView)
        self.view.addSubview(self.button)
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    func refreshDataSource() -> Void {
        if let _ = self.dataSource {
            if !self.dataSource.isEmpty {
                self.dataSource.removeAll()
            }
        }
        else
        {
            self.dataSource = [Int]()
        }

        for count in 0..<self.count {
            self.dataSource.append(count)
        }

        self.tableViewController.dataSource = self.dataSource
        self.tableViewController.tableView.reloadData()
        if let _ = self.view {
            self.layoutFrames()
            self.view.setNeedsDisplay()
        }
    }

    func updateTable() -> Void {
        guard let _ = self.textView.text else { return }
        guard let validNumber = Int(self.textView.text!) else { return }

        self.count = validNumber
    }

    func layoutFrames() -> Void {

        if self.tableViewController.tableView != nil {
            self.tableViewController.tableView.frame = CGRect(origin: CGPoint(x: self.view.frame.width / 2 - 100, y: 100), size: CGSize(width: 200, height: CGFloat(self.dataSource.count) * self.rowHeight))
            NSLog("\(self.tableViewController.tableView.frame)")
        }

        if self.textView != nil {
            self.textView.frame = CGRect(origin: CGPoint(x: 50, y: 100), size: CGSize(width: 100, height: 100))
        }

        if self.button != nil {
            self.button.frame = CGRect(origin: CGPoint(x: 50, y: 150), size: CGSize(width: 100, height: 100))
        }
    }
}

class FlexibleTableViewController : UITableViewController {

    var dataSource : [Int]!

    override init(style: UITableViewStyle) {
        super.init(style: style)
    }

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

    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.dataSource.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")

        let cell = tableView.dequeueReusableCell(withIdentifier: "cell") ?? UITableViewCell()

        cell.frame = CGRect(origin: CGPoint(x: 10, y: 5), size: CGSize(width: 180, height : 40))
        cell.backgroundColor = UIColor.green

        return cell
    }

}

Whether it is a good idea or not, is, as has been pointed out, another question! Hope that helps!

Marcus
  • 2,153
  • 2
  • 13
  • 21
  • This seems a bit complex, and doesn't appear to use AutoLayout which was the main point. I'm posting an answer that is similar to this (uses the row count * rowHeight) but does so via the intrinsic size. – Mark A. Donohoe Sep 22 '16 at 04:20
0

Version from no_ripcord accounting for header and footer height

final // until proven otherwise
class IntrinsicallySizedTableView: UITableView {

  override func layoutSubviews() {
    super.layoutSubviews()
    self.invalidateIntrinsicContentSize()
  }
  
  override var intrinsicContentSize: CGSize {
    guard let dataSource = self.dataSource else {
      return super.intrinsicContentSize
    }
    var height: CGFloat = (tableHeaderView?.intrinsicContentSize.height ?? 0)
      + contentInset.top + contentInset.bottom
    if let footer = tableFooterView {
      height += footer.intrinsicContentSize.height
    }
    let nsections = dataSource.numberOfSections?(in: self) ?? self.numberOfSections
    for section in 0..<nsections {
      let sectionheader = rectForHeader(inSection: section)
      height += sectionheader.height
      let sectionfooter = rectForFooter(inSection: section)
      height += sectionfooter.height
      let nRowsSection = self.numberOfRows(inSection: section)
      for row in 0..<nRowsSection {
        height += self.rectForRow(at: IndexPath(row: row, section: section)).size.height
      }
    }
    return CGSize(width: UIView.noIntrinsicMetric, height: height)
  }
}
Anton Tropashko
  • 5,486
  • 5
  • 41
  • 66