开发者

Resizing UITableView to fit content

I am creating an app which will have a question in a UILabel and a multiple choice answers displayed in UITableView, each row showing a multiple choice. Questions and answers will vary, so I need this UITableView to be dynamic in height.

I would like to find a sizeToF开发者_开发问答it work around for the table. Where the table's frame is set to the height of all it's content.

Can anyone advise on how I can achieve this?


Swift 5 and 4.2 solution without KVO, DispatchQueue, or setting constraints yourself.

This solution is based on Gulz's answer.

1) Create a subclass of 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)
    }
}

2) Add a UITableView to your layout and set constraints on all sides. Set the class of it to ContentSizedTableView.

3) You should see some errors, because Storyboard doesn't take our subclass' intrinsicContentSize into account. Fix this by opening the size inspector and overriding the intrinsicContentSize to a placeholder value. This is an override for design time. At runtime it will use the override in our ContentSizedTableView class


Update: Changed code for Swift 4.2. If you're using a prior version, use UIViewNoIntrinsicMetric instead of UIView.noIntrinsicMetric


Actually I found the answer myself.

I just create a new CGRect for the tableView.frame with the height of table.contentSize.height

That sets the height of the UITableView to the height of its content. Since the code modifies the UI, do not forget to run it in the main thread:

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;
    });


Swift Solution

Follow these steps:

  1. Set the height constraint for the table from the storyboard.

  2. Drag the height constraint from the storyboard and create @IBOutlet for it in the view controller file.

    @IBOutlet var tableHeight: NSLayoutConstraint!
    
  3. Then you can change the height for the table dynamicaly using this code:

    override func viewWillLayoutSubviews() {
        super.updateViewConstraints()
        self.tableHeight?.constant = self.table.contentSize.height
    }
    

If the last row is cut off, try to call viewWillLayoutSubviews() in willDisplay cell function:

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


I've tried this in iOS 7 and it worked for me

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


Add an observer for the contentSize property on the table view, and adjust the frame size accordingly

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

then in the callback:

- (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;
    }

Hope this will help you.


I had a table view inside scroll view and had to calculate tableView's height and resize it accordingly. Those are steps I've taken:

0) add a UIView to your scrollView (probably will work without this step but i did it to avoid any possible conflicts) - this will be a containr view for your table view. If you take this step , then set the views borders right to tableview's ones.

1) create a subclass of 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) set class of a table view in Storyboard to IntrinsicTableView: screenshot: http://joxi.ru/a2XEENpsyBWq0A

3) Set the heightConstraint to your table view

4) drag the IBoutlet of your table to your ViewController

5) drag the IBoutlet of your table's height constraint to your ViewController

6) add this method into your ViewController:

override func viewWillLayoutSubviews() {
        super.updateViewConstraints()
        self.yourTableViewsHeightConstraint?.constant = self.yourTableView.intrinsicContentSize.height
    }

Hope this helps


Swift 5 Solution

Follow these four steps:

  1. Set the height constraint for the tableview from the storyboard.

  2. Drag the height constraint from the storyboard and create @IBOutlet for it in the view controller file.

    @IBOutlet var tableViewHeightConstraint: NSLayoutConstraint!
    
  3. Add an observer for the contentSize property on the override func viewDidLoad()

override func viewDidLoad() {
        super.viewDidLoad()
        self.tableView.addObserver(self, forKeyPath: "contentSize", options: .new, context: nil)
 
    }

  1. Then you can change the height for the table dynamicaly using this code:

    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
                 }
    
             }
         }
     }
    


In case you don't want to track table view's content size changes yourself, you might find this subclass useful.

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
    }
}


In case your contentSize is not correct this is because it is based on the estimatedRowHeight (automatic), use this before

tableView.estimatedRowHeight = 0;

source : https://forums.developer.apple.com/thread/81895


I did in a bit different way, Actually my TableView was inside scrollview so i had to give height constraint as 0.

Then at runtime I made following changes,

       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()
          }
       }


Swift 3, iOS 10.3

Solution 1: Just put self.tableview.sizeToFit() in cellForRowAt indexPath function. Make sure to set tableview height higher then you need. This is a good solution if you don't have views below tableview. However, if you have, bottom tableview constraint will not be updated (I didn't try to fix it because I came up with solution 2)

Example:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    if let cell = tableView.dequeueReusableCell(withIdentifier: "TestCell", for: indexPath) as? TestCell {
        cell.configureCell(data: testArray[indexPath.row])
        self.postsTableView.sizeToFit()
        return cell
    }

    return UITableViewCell()
}

Solution 2: Set tableview height constraint in storyboard and drag it to the ViewController. If you know the average height of your cell and you know how many elements your array contains, you can do something like this:

tableViewHeightConstraint.constant = CGFloat(testArray.count) * 90.0     // Let's say 90 is the average cell height

*EDIT:

After all the solutions I tried and every of them was fixing something, but not completely, this is the answer that explains and fixes this problem completely.


This works for me using Auto Layout, with a table view with only one section.

func getTableViewContentHeight(tableView: UITableView) -> CGFloat {
        tableView.bounds = CGRect(x: 0, y: 0, width: 300, height: 40)
        let rows = tableView.numberOfRows(inSection: 0)
        var height = CGFloat(0)
        for n in 0...rows - 1 {
            height = height + tableView.rectForRow(at: IndexPath(row: n, section: 0)).height
        }
        return height
    }

I call this function when setting up Auto Layout (The sample here uses SnapKit, but you get the idea):

    let height = getTableViewContentHeight(tableView: myTableView)
    myTableView.snp.makeConstraints {
        ...
        ...
        $0.height.equalTo(height)
    }

I want the UITableView only to be as tall as the combined height of the cells; I loop through the cells and accumulate the total height of the cells. Since the size of the table view is CGRect.zero at this point, I need to set the bounds to be able to respect the Auto Layout rules defined by the cell. I set the size to an arbitrary value that should be large enough. The actual size will be calculated later by the Auto Layout system.


There is a much better way to do it if you use AutoLayout: change the constraint that determines the height. Just calculate the height of your table contents, then find the constraint and change it. Here's an example (assuming that the constraint that determines your table's height is actually a height constraint with relation "Equal"):

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    for constraint in tableView.constraints {
        if constraint.firstItem as? UITableView == tableView {
            if constraint.firstAttribute == .height {
                constraint.constant = tableView.contentSize.height
            }
        }
    }
}


based on fl034's answer

SWift 5

var tableViewHeight: NSLayoutConstraint?

    tableViewHeight = NSLayoutConstraint(item: servicesTableView, 
    attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute,
    multiplier: 0.0, constant: 10)
    tableViewHeight?.isActive = true


  func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    tableViewHeight?.constant = tableView.contentSize.height
    tableView.layoutIfNeeded()
}


Mimo's answer and Anooj VM 's answer both are awesome but there is a small problem if you have a large list, it's possible that the height of the frame will cutoff some of your cells.

So. I have modified the answer a little bit:

dispatch_async(dispatch_get_main_queue()) {
    //This code will run in the main thread:
    CGFloat newHeight=self.tableView.contentSize.height;
    CGFloat screenHeightPermissible=(self.view.bounds.size.height-self.tableView.frame.origin.y);
    if (newHeight>screenHeightPermissible)
    {
        //so that table view remains scrollable when 'newHeight'  exceeds the screen bounds
        newHeight=screenHeightPermissible;
    }

    CGRect frame = self.tableView.frame;
    frame.size.height = newHeight;
    self.tableView.frame = frame;
}


My Swift 5 implementation is to set the hight constraint of the tableView to the size of its content (contentSize.height). This method assumes you are using auto layout. This code should be placed inside the cellForRowAt tableView method.

tableView.heightAnchor.constraint(equalToConstant: tableView.contentSize.height).isActive = true


As an extension of Anooj VM's answer, I suggest the following to refresh content size only when it changes.

This approach also disable scrolling properly and support larger lists and rotation. There is no need to dispatch_async because contentSize changes are dispatched on main thread.

- (void)viewDidLoad {
        [super viewDidLoad];
        [self.tableView addObserver:self forKeyPath:@"contentSize" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:NULL]; 
}


- (void)resizeTableAccordingToContentSize:(CGSize)newContentSize {
        CGRect superviewTableFrame  = self.tableView.superview.bounds;
        CGRect tableFrame = self.tableView.frame;
        BOOL shouldScroll = newContentSize.height > superviewTableFrame.size.height;
        tableFrame.size = shouldScroll ? superviewTableFrame.size : newContentSize;
        [UIView animateWithDuration:0.3
                                    delay:0
                                    options:UIViewAnimationOptionCurveLinear
                                    animations:^{
                            self.tableView.frame = tableFrame;
        } completion: nil];
        self.tableView.scrollEnabled = shouldScroll;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
    if ([change[NSKeyValueChangeKindKey] unsignedIntValue] == NSKeyValueChangeSetting &&
        [keyPath isEqualToString:@"contentSize"] &&
        !CGSizeEqualToSize([change[NSKeyValueChangeOldKey] CGSizeValue], [change[NSKeyValueChangeNewKey] CGSizeValue])) {
        [self resizeTableAccordingToContentSize:[change[NSKeyValueChangeNewKey] CGSizeValue]];
    } 
}

- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
    [super didRotateFromInterfaceOrientation:fromInterfaceOrientation];
    [self resizeTableAccordingToContentSize:self.tableView.contentSize]; }

- (void)dealloc {
    [self.tableView removeObserver:self forKeyPath:@"contentSize"];
}


objc version of Musa almatri

(void)viewWillLayoutSubviews
{
    [super updateViewConstraints];
    CGFloat desiredHeight = self.tableView.contentSize.height;
    // clamp desired height, if needed, and, in that case, leave scroll Enabled
    self.tableHeight.constant = desiredHeight;
    self.tableView.scrollEnabled = NO;
}


You can try Out this Custom AGTableView

To Set a TableView Height Constraint Using storyboard or programmatically. (This class automatically fetch a height constraint and set content view height to yourtableview height).

class AGTableView: UITableView {

    fileprivate var heightConstraint: NSLayoutConstraint!

    override init(frame: CGRect, style: UITableViewStyle) {
        super.init(frame: frame, style: style)
        self.associateConstraints()
    }

    required public init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.associateConstraints()
    }

    override open func layoutSubviews() {
        super.layoutSubviews()

        if self.heightConstraint != nil {
            self.heightConstraint.constant = self.contentSize.height
        }
        else{
            self.sizeToFit()
            print("Set a heightConstraint to Resizing UITableView to fit content")
        }
    }

    func associateConstraints() {
        // iterate through height constraints and identify

        for constraint: NSLayoutConstraint in constraints {
            if constraint.firstAttribute == .height {
                if constraint.relation == .equal {
                    heightConstraint = constraint
                }
            }
        }
    }
}

Note If any problem to set a Height then yourTableView.layoutSubviews().


Based on answer of fl034. But for Xamarin.iOS users:

    [Register("ContentSizedTableView")]
    public class ContentSizedTableView : UITableView
    {
        public ContentSizedTableView(IntPtr handle) : base(handle)
        {
        }

        public override CGSize ContentSize { get => base.ContentSize; set { base.ContentSize = value; InvalidateIntrinsicContentSize(); } }
        public override CGSize IntrinsicContentSize
        {
            get
            {
                this.LayoutIfNeeded();
                return new CGSize(width: NoIntrinsicMetric, height: ContentSize.Height);
            }
        }
    }


I am using a UIView extension , approach is close to @ChrisB approach above

 extension UIView {
func updateHeight(_ height:NSLayoutConstraint)
{
    
    let newSize = CGSize(width: self.frame.size.width, height: CGFloat(MAXFLOAT))
    let fitSize : CGSize = self.sizeThatFits(newSize)
    
    height.constant = fitSize.height
    
   
}
}

implementation : :

@IBOutlet weak var myTableView: UITableView!
@IBOutlet weak var myTableVieweHeight: NSLayoutConstraint!
//(call it whenever tableView is updated inside/outside delegate methods)
myTableView.updateHeight(myTableVieweHeigh)

Bonus : Can be used on any other UIViews eg:your own dynamic label


For my case, how I manage is. give any constant height of table view. create outlet of table view height and then call the following function where ever you relaod the tableView.

private func manageHeight(){
        tableViewHeight.constant=CGFloat.greatestFiniteMagnitude
        tableView.reloadData()
        tableView.layoutIfNeeded()
        tableViewHeight.constant=tableView.contentSize.height
    }

note: tableView is the outlet for your table view and tableViewHeight is the outlet for tableView height.


If you want your table to be dynamic, you will need to use a solution based on the table contents as detailed above. If you simply want to display a smaller table, you can use a container view and embed a UITableViewController in it - the UITableView will be resized according to the container size.

This avoids a lot of calculations and calls to layout.


Mu solution for this in swift 3: Call this method in viewDidAppear

func UITableView_Auto_Height(_ t : UITableView)
{
        var frame: CGRect = t.frame;
        frame.size.height = t.contentSize.height;
        t.frame = frame;        
}
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜