Using Auto Layout in UITableView for dynamic cell layouts & variable row heights

来源:互联网 发布:飚风打印软件 编辑:程序博客网 时间:2024/05/24 05:07

How do you use Auto Layout within UITableViewCells in a table view to let each cell's content and subviews determine the row height, while maintaining smooth scrolling performance?


Answers:

TL;DR: Don't like reading? Jump straight to the sample projects on GitHub:

  • iOS 8 Sample Project - Requires iOS 8
  • iOS 7 Sample Project - Works on iOS 7+

Conceptual Description

The first 2 steps below are applicable regardless of which iOS versions you are developing for.

1. Set Up & Add Constraints

In your UITableViewCell subclass, add constraints so that the subviews of the cell have their edges pinned to the edges of the cell's contentView (most importantly to the top AND bottom edges). NOTE: don't pin subviews to the cell itself; only to the cell's contentView! Let the intrinsic content size of these subviews drive the height of the table view cell's content view by making sure the content compression resistance and content hugging constraints in the vertical dimension for each subview are not being overridden by higher-priority constraints you have added. (Huh? Click here.)

Remember, the idea is to have the cell's subviews connected vertically to the cell's content view so that they can "exert pressure" and make the content view expand to fit them. Using an example cell with a few subviews, here is a visual illustration of what some (not all!) of your constraints would need to look like:

Example illustration of constraints on a table view cell.

You can imagine that as more text is added to the multi-line body label in the example cell above, it will need to grow vertically to fit the text, which will effectively force the cell to grow in height. (Of course, you need to get the constraints right in order for this to work correctly!)

Getting your constraints right is definitely the hardest and most important part of getting dynamic cell heights working with Auto Layout. If you make a mistake here, it could prevent everything else from working -- so take your time! I recommend setting up your constraints in code because you know exactly which constraints are being added where, and it's a lot easier to debug when things go wrong. Adding constraints in code is just as easy as and significantly more powerful than Interface Builder when you leverage one of the fantastic open source APIs available -- here is the one I design, maintain, and use exclusively: https://github.com/smileyborg/PureLayout

  • If you're adding constraints in code, you should do this once from within the updateConstraintsmethod of your UITableViewCell subclass. Note that updateConstraints may be called more than once, so to avoid adding the same constraints more than once, make sure to wrap your constraint-adding code within updateConstraints in a check for a boolean property such as didSetupConstraints (which you set to YES after you run your constraint-adding code once). On the other hand, if you have code that updates existing constraints (such as adjusting the constant property on some constraints), place this in updateConstraints but outside of the check for didSetupConstraints so it can run every time the method is called.

2. Determine Unique Table View Cell Reuse Identifiers

For every unique set of constraints in the cell, use a unique cell reuse identifier. In other words, if your cells have more than one unique layout, each unique layout should receive its own reuse identifier. (A good hint that you need to use a new reuse identifier is when your cell variant has a different number of subviews, or the subviews are arranged in a distinct fashion.)

For example, if you were displaying an email message in each cell, you might have 4 unique layouts: messages with just a subject, messages with a subject and a body, messages with a subject and a photo attachment, and messages with a subject, body, and photo attachment. Each layout has completely different constraints required to achieve it, so once the cell is initialized and the constraints are added for one of these cell types, the cell should get a unique reuse identifier specific to that cell type. This means when you dequeue a cell for reuse, the constraints have already been added and are ready to go for that cell type.

Note that due to differences in intrinsic content size, cells with the same constraints (type) may still have varying heights! Don't confuse fundamentally different layouts (different constraints) with different calculated view frames (solved from identical constraints) due to different sizes of content.

  • Do not add cells with completely different sets of constraints to the same reuse pool (i.e. use the same reuse identifier) and then attempt to remove the old constraints and set up new constraints from scratch after each dequeue. The internal Auto Layout engine is not designed to handle large scale changes in constraints, and you will see massive performance issues.

For iOS 8 - Self-Sizing Cells

3. Enable Row Height Estimation

With iOS 8, Apple has internalized much of the work that previously had to be implemented by you prior to iOS 8. In order to allow the self-sizing cell mechanism to work, you must first set the rowHeightproperty on the table view to the constant UITableViewAutomaticDimension. Then, you simply need to enable row height estimation by setting the table view's estimatedRowHeight property to a nonzero value, for example:

self.tableView.rowHeight = UITableViewAutomaticDimension;self.tableView.estimatedRowHeight = 44.0; // set to whatever your "average" cell height is

What this does is provide the table view with a temporary estimate/placeholder for the row heights of cells that are not yet onscreen. Then, when these cells are about to scroll onscreen, the actual row height will be calculated. To determine the actual height for each row, the table view automatically asks each cell what height its contentView needs to be based on the known fixed width of the content view (which is based on the table view's width, minus any additional things like a section index or accessory view) and the auto layout constraints you have added to the cell's content view and subviews. Once this actual cell height has been determined, the old estimated height for the row is updated with the new actual height (and any adjustments to the table view's contentSize/contentOffset are made as needed for you).

Generally speaking, the estimate you provide doesn't have to be very accurate -- it is only used to correctly size the scroll indicator in the table view, and the table view does a good job of adjusting the scroll indicator for incorrect estimates as you scroll cells onscreen. You should set the estimatedRowHeight property on the table view (in viewDidLoad or similar) to a constant value that is the "average" row height. Only if your row heights have extreme variability (e.g. differ by an order of magnitude) and you notice the scroll indicator "jumping" as you scroll should you bother implementing tableView:estimatedHeightForRowAtIndexPath: to do the minimal calculation required to return a more accurate estimate for each row.

For iOS 7 support (implementing auto cell sizing yourself)

3. Do a Layout Pass & Get The Cell Height

First, instantiate an offscreen instance of a table view cell, one instance for each reuse identifier, that is used strictly for height calculations. (Offscreen meaning the cell reference is stored in a property/ivar on the view controller and never returned from tableView:cellForRowAtIndexPath: for the table view to actually render onscreen.) Next, the cell must be configured with the exact content (e.g. text, images, etc) that it would hold if it were to be displayed in the table view.

Then, force the cell to immediately layout its subviews, and then use the systemLayoutSizeFittingSize: method on the UITableViewCell's contentView to find out what the required height of the cell is. Use UILayoutFittingCompressedSize to get the smallest size required to fit all the contents of the cell. The height can then be returned from the tableView:heightForRowAtIndexPath: delegate method.

4. Use Estimated Row Heights

If your table view has more than a couple dozen rows in it, you will find that doing the Auto Layout constraint solving can quickly bog down the main thread when first loading the table view, as tableView:heightForRowAtIndexPath: is called on each and every row upon first load (in order to calculate the size of the scroll indicator).

As of iOS 7, you can (and absolutely should) use the estimatedRowHeight property on the table view. What this does is provide the table view with a temporary estimate/placeholder for the row heights of cells that are not yet onscreen. Then, when these cells are about to scroll onscreen, the actual row height will be calculated (by calling tableView:heightForRowAtIndexPath:), and the estimated height updated with the actual one.

Generally speaking, the estimate you provide doesn't have to be very accurate -- it is only used to correctly size the scroll indicator in the table view, and the table view does a good job of adjusting the scroll indicator for incorrect estimates as you scroll cells onscreen. You should set the estimatedRowHeight property on the table view (in viewDidLoad or similar) to a constant value that is the "average" row height. Only if your row heights have extreme variability (e.g. differ by an order of magnitude) and you notice the scroll indicator "jumping" as you scroll should you bother implementing tableView:estimatedHeightForRowAtIndexPath: to do the minimal calculation required to return a more accurate estimate for each row.

5. (If Needed) Add Row Height Caching

If you've done all the above and are still finding that performance is unacceptably slow when doing the constraint solving in tableView:heightForRowAtIndexPath:, you'll unfortunately need to implement some caching for cell heights. (This is the approach suggested by Apple's engineers.) The general idea is to let the Auto Layout engine solve the constraints the first time, then cache the calculated height for that cell and use the cached value for all future requests for that cell's height. The trick of course is to make sure you clear the cached height for a cell when anything happens that could cause the cell's height to change -- primarily, this would be when that cell's content changes or when other important events occur (like the user adjusting the Dynamic Type text size slider).

iOS 7 Generic Sample Code (with lots of juicy comments)

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{    // Determine which reuse identifier should be used for the cell at this index path,    // depending on the particular layout required (you may have just one, or may have many).    NSString *reuseIdentifier = ...;    // Dequeue a cell for the reuse identifier.    // Note that this method will init and return a new cell if there isn't one available in the reuse pool,    // so either way after this line of code you will have a cell with the correct constraints ready to go.    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier];    // Configure the cell with content for the given indexPath, for example:    // cell.textLabel.text = someTextForThisCell;    // ...    // Make sure the constraints have been set up for this cell, since it may have just been created from scratch.    // Use the following lines, assuming you are setting up constraints from within the cell's updateConstraints method:    [cell setNeedsUpdateConstraints];    [cell updateConstraintsIfNeeded];    // If you are using multi-line UILabels, don't forget that the preferredMaxLayoutWidth needs to be set correctly.    // Do it at this point if you are NOT doing it within the UITableViewCell subclass -[layoutSubviews] method.    // For example:    // cell.multiLineLabel.preferredMaxLayoutWidth = CGRectGetWidth(tableView.bounds);    return cell;}- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{    // Determine which reuse identifier should be used for the cell at this index path.    NSString *reuseIdentifier = ...;    // Use a dictionary of offscreen cells to get a cell for the reuse identifier, creating a cell and storing    // it in the dictionary if one hasn't already been added for the reuse identifier.    // WARNING: Don't call the table view's dequeueReusableCellWithIdentifier: method here because this will result    // in a memory leak as the cell is created but never returned from the tableView:cellForRowAtIndexPath: method!    UITableViewCell *cell = [self.offscreenCells objectForKey:reuseIdentifier];    if (!cell) {        cell = [[YourTableViewCellClass alloc] init];        [self.offscreenCells setObject:cell forKey:reuseIdentifier];    }    // Configure the cell with content for the given indexPath, for example:    // cell.textLabel.text = someTextForThisCell;    // ...    // Make sure the constraints have been set up for this cell, since it may have just been created from scratch.    // Use the following lines, assuming you are setting up constraints from within the cell's updateConstraints method:    [cell setNeedsUpdateConstraints];    [cell updateConstraintsIfNeeded];    // Set the width of the cell to match the width of the table view. This is important so that we'll get the    // correct cell height for different table view widths if the cell's height depends on its width (due to    // multi-line UILabels word wrapping, etc). We don't need to do this above in -[tableView:cellForRowAtIndexPath]    // because it happens automatically when the cell is used in the table view.    // Also note, the final width of the cell may not be the width of the table view in some cases, for example when a    // section index is displayed along the right side of the table view. You must account for the reduced cell width.    cell.bounds = CGRectMake(0.0f, 0.0f, CGRectGetWidth(tableView.bounds), CGRectGetHeight(cell.bounds));    // Do the layout pass on the cell, which will calculate the frames for all the views based on the constraints.    // (Note that you must set the preferredMaxLayoutWidth on multi-line UILabels inside the -[layoutSubviews] method    // of the UITableViewCell subclass, or do it manually at this point before the below 2 lines!)    [cell setNeedsLayout];    [cell layoutIfNeeded];    // Get the actual height required for the cell's contentView    CGFloat height = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;    // Add an extra point to the height to account for the cell separator, which is added between the bottom    // of the cell's contentView and the bottom of the table view cell.    height += 1.0f;    return height;}// NOTE: Set the table view's estimatedRowHeight property instead of implementing the below method, UNLESS// you have extreme variability in your row heights and you notice the scroll indicator "jumping" as you scroll.- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath{    // Do the minimal calculations required to be able to return an estimated row height that's    // within an order of magnitude of the actual height.    // For example:    if ([self isTallCellAtIndexPath:indexPath]) {        return 350.0f;    } else {        return 40.0f;    }}

Sample Projects

  • iOS 8 Sample Project - Requires iOS 8
  • iOS 7 Sample Project - Works on iOS 7+

These projects are fully working examples of table views with variable row heights due to table view cells containing dynamic content in UILabels.

Feel free to raise any questions or issues you run into (you can open issues on GitHub or post comments here). I'll try my best to help!

Xamarin (C#/.NET)

If you're using Xamarin, check out this sample project put together by @KentBoogaart.

share|improve this answer
 
2 
@smileyborg, thanks for your well explained post but I'm not reproducing the success. As a matter of clarity, I've created a sample project which demonstrates my constraints are set correctly, yet, the return value of [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height; appears to be erratic. Sometimes returning 0, other times returning 81. I wonder if you might provide some details as I might be missing a key concept? github.com/caoimghgin/TableViewCellWithAutoLayout –  caoimghgin Oct 5 '13 at 21:31
4 
If I was 95% the way there, the remaining 5% would have taken me half a week to figure out! I really appreciate you throwing in and lending a hand so quickly. Working code is up on GitHub so and the rest of the community can benefit. Really great work smileyborg! –  caoimghgin Oct 6 '13 at 2:18
3 
@Alex311 Very interesting, thanks for providing this example. I did a little testing on my end and wrote up some comments here: github.com/Alex311/TableCellWithAutoLayout/commit/… –  smileyborg Nov 18 '13 at 21:31
13 
Wow, I cannot believe how complicated this is. Thanks for such a great post. –  Nathan Buggia Apr 16 at 4:32
7 
+1. And by "+1" I mean "I want to give you a hug as I wipe the tears of frustration from my eyes." –  AaronSep 22 at 0:44
up vote8down vote

The solution proposed by @smileyborg it's almost perfect. If you have a custom cell and you want one or more UILabel with dynamic heights then the systemLayoutSizeFittingSize method combined with autolayout enabled returns a CGSizeZero unless you move all your cell constraints from the cell to its contentView (as suggested by @TomSwift here How to resize superview to fit all subviews with autolayout?).

To do so you need to insert the following code in your custom UITableViewCell implementation (thanks to @Adrian).

-(void)awakeFromNib{    [super awakeFromNib];    for(NSLayoutConstraint *cellConstraint in self.constraints){        [self removeConstraint:cellConstraint];        id firstItem = cellConstraint.firstItem == self ? self.contentView : cellConstraint.firstItem;        id seccondItem = cellConstraint.secondItem == self ? self.contentView : cellConstraint.secondItem;        NSLayoutConstraint* contentViewConstraint =        [NSLayoutConstraint constraintWithItem:firstItem                                 attribute:cellConstraint.firstAttribute                                 relatedBy:cellConstraint.relation                                    toItem:seccondItem                                 attribute:cellConstraint.secondAttribute                                multiplier:cellConstraint.multiplier                                  constant:cellConstraint.constant];        [self.contentView addConstraint:contentViewConstraint];    }}

Mixing @smileyborg answer with this should works.

share|improve this answer
 
1 
Another way to summarize your answer: Interface Builder is adding your constraints to the UITableViewCell itself, instead of the cell's contentView (which would make much more sense), and this is incompatible with the dynamic cell sizing mechanism I describe. My recommendation would be to add all your constraints in code instead of from IB -- it's much easier and less painful, especially when using a developer-friendly API such the one I've designed: github.com/smileyborg/UIView-AutoLayout –  smileyborgNov 11 '13 at 20:28 
1 
One reason to be cautious with the above code snippet is that removing many constraints can cause extremely bad performance issues due to the internal Auto Layout engine's implementation. You really just want to set them up correctly once, and only make changes when absolutely necessary. –  smileyborg Nov 11 '13 at 20:30
 
Yes I agree with you about performance issues but I would like to explain myself. It's not so hard to set constraints from IB and with the latest XCode 5 update IB adds constraints to the ContentView smoothly; with the previous XCode 5 release, IB was adding constraints to the UITableViewCell itself instead of the cell's contentView and that's where my issues come from. I've decided to use this snippet instead of changing constraints from IB to avoid messing with new constraints. Anyway I think this snippet could be useful to others having my same problem. –  wildmonkey Nov 14 '13 at 13:18
2 
Yes, that makes sense. With the latest Xcode 5 IB update, have you tested & confirmed that when adding constraints to a table view cell's subviews, they actually get installed on the contentView? Assuming yes, then this code shouldn't be needed. –  smileyborg Nov 14 '13 at 17:26
2 
yes, confirmed! –  wildmonkey Nov 15 '13 at 14:00
up vote7down vote

I wrapped @smileyborg's iOS7 solution in a category

In the interest of separating concerns I decided to wrap this clever solution by @smileyborg into a UICollectionViewCell+AutoLayoutDynamicHeightCalculation category.

The category also rectifies the issues outlined in @wildmonkey's answer (loading a cell from a nib and systemLayoutSizeFittingSize: returning CGRectZero)

It doesn't take into account any caching but suits my needs right now. Feel free to copy, paste and hack at it.

UICollectionViewCell+AutoLayoutDynamicHeightCalculation.h

#import <UIKit/UIKit.h>typedef void (^UICollectionViewCellAutoLayoutRenderBlock)(void);/** *  A category on UICollectionViewCell to aid calculating dynamic heights based on AutoLayout contraints. * *  Many thanks to @smileyborg and @wildmonkey * *  @see stackoverflow.com/questions/18746929/using-auto-layout-in-uitableview-for-dynamic-cell-layouts-variable-row-heights */@interface UICollectionViewCell (AutoLayoutDynamicHeightCalculation)/** *  Grab an instance of the receiving type to use in order to calculate AutoLayout contraint driven dynamic height. The method pulls the cell from a nib file and moves any Interface Builder defined contrainsts to the content view. * *  @param name Name of the nib file. * *  @return collection view cell for using to calculate content based height */+ (instancetype)heightCalculationCellFromNibWithName:(NSString *)name;/** *  Returns the height of the receiver after rendering with your model data and applying an AutoLayout pass * *  @param block Render the model data to your UI elements in this block * *  @return Calculated constraint derived height */- (CGFloat)heightAfterAutoLayoutPassAndRenderingWithBlock:(UICollectionViewCellAutoLayoutRenderBlock)block collectionViewWidth:(CGFloat)width;/** *  Directly calls `heightAfterAutoLayoutPassAndRenderingWithBlock:collectionViewWidth` assuming a collection view width spanning the [UIScreen mainScreen] bounds */- (CGFloat)heightAfterAutoLayoutPassAndRenderingWithBlock:(UICollectionViewCellAutoLayoutRenderBlock)block;@end

UICollectionViewCell+AutoLayoutDynamicHeightCalculation.m

#import "UICollectionViewCell+AutoLayout.h"@implementation UICollectionViewCell (AutoLayout)#pragma mark Dummy Cell Generator+ (instancetype)heightCalculationCellFromNibWithName:(NSString *)name{    UICollectionViewCell *heightCalculationCell = [[[NSBundle mainBundle] loadNibNamed:name owner:self options:nil] lastObject];    [heightCalculationCell moveInterfaceBuilderLayoutConstraintsToContentView];    return heightCalculationCell;}#pragma mark Moving Constraints- (void)moveInterfaceBuilderLayoutConstraintsToContentView{    [self.constraints enumerateObjectsUsingBlock:^(NSLayoutConstraint *constraint, NSUInteger idx, BOOL *stop) {        [self removeConstraint:constraint];        id firstItem = constraint.firstItem == self ? self.contentView : constraint.firstItem;        id secondItem = constraint.secondItem == self ? self.contentView : constraint.secondItem;        [self.contentView addConstraint:[NSLayoutConstraint constraintWithItem:firstItem                                                                     attribute:constraint.firstAttribute                                                                     relatedBy:constraint.relation                                                                        toItem:secondItem                                                                     attribute:constraint.secondAttribute                                                                    multiplier:constraint.multiplier                                                                      constant:constraint.constant]];    }];}#pragma mark Height- (CGFloat)heightAfterAutoLayoutPassAndRenderingWithBlock:(UICollectionViewCellAutoLayoutRenderBlock)block{    return [self heightAfterAutoLayoutPassAndRenderingWithBlock:block                                            collectionViewWidth:CGRectGetWidth([[UIScreen mainScreen] bounds])];}- (CGFloat)heightAfterAutoLayoutPassAndRenderingWithBlock:(UICollectionViewCellAutoLayoutRenderBlock)block collectionViewWidth:(CGFloat)width{    NSParameterAssert(block);    block();    [self setNeedsUpdateConstraints];    [self updateConstraintsIfNeeded];    self.bounds = CGRectMake(0.0f, 0.0f, width, CGRectGetHeight(self.bounds));    [self setNeedsLayout];    [self layoutIfNeeded];    CGSize calculatedSize = [self.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];    return calculatedSize.height;}@end

Usage example:

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath{    MYSweetCell *cell = [MYSweetCell heightCalculationCellFromNibWithName:NSStringFromClass([MYSweetCell class])];    CGFloat height = [cell heightAfterAutoLayoutPassAndRenderingWithBlock:^{        [(id<MYSweetCellRenderProtocol>)cell renderWithModel:someModel];    }];    return CGSizeMake(CGRectGetWidth(self.collectionView.bounds), height);}

Thankfully we won't have to do this jazz in iOS8, but there it is for now!

share|improve this answer
 
 
+1. I have an additional question: How can the Dummy Cell Generator be adapted for a completely programmatic UICollectionView (containing programmatic UICollectionViewCells)? How to init the dummy cell without a nib to load? –  Ricardo Sánchez-Sáez Jul 9 at 16:58 
1 
You should just be able to simply use a: [YourCell new] and use that as the dummy. As long as the constraint code building code is fired in your instance, and you trigger a layout pass programmatically you should be good to go. –  Adam Waite Jul 9 at 18:32 
1 
Thanks! This works. Your category is great. It is what made me realize that this technique works with UICollectionViews as well. –  Ricardo Sánchez-Sáez Jul 9 at 18:52
 
Great stuff, glad to help. –  Adam Waite Jul 9 at 20:19
up vote4down vote

An important enough gotcha I just ran into to post as an answer.

@smileyborg's answer is mostly correct. However, if you have any code in the layoutSubviewsmethod of your custom cell class, for instance setting the preferredMaxLayoutWidth, then it won't be run with this code:

[cell.contentView setNeedsLayout];[cell.contentView layoutIfNeeded];

It confounded me for awhile. Then I realized it's because those are only triggering layoutSubviews on the contentView, not the cell itself.

My working code looks like this:

TCAnswerDetailAppSummaryCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"TCAnswerDetailAppSummaryCell"];[cell configureWithThirdPartyObject:self.app];[cell layoutIfNeeded];CGFloat height = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;return height;

Note that if you are creating a new cell, I'm pretty sure you don't need to call setNeedsLayout as it should already be set. In cases where you save a reference to a cell, you should probably call it. Either way it shouldn't hurt anything.

Another tip if you are using cell subclasses where you are setting things like preferredMaxLayoutWidth. As @smileyborg mentions, "your table view cell hasn't yet had its width fixed to the table view's width". This is true, and trouble if you are doing your work in your subclass and not in the view controller. However you can simply set the cell frame at this point using the table width:

For instance in the calculation for height:

self.summaryCell = [self.tableView dequeueReusableCellWithIdentifier:@"TCAnswerDetailDefaultSummaryCell"];CGRect oldFrame = self.summaryCell.frame;self.summaryCell.frame = CGRectMake(oldFrame.origin.x, oldFrame.origin.y, self.tableView.frame.size.width, oldFrame.size.height);

(I happen to cache this particular cell for re-use, but that's irrelevant).

share|improve this answer
 
up vote3down vote

In case people are still having trouble with this. I wrote a quick blog post about using Autolayout with UITableViews Leveraging Autolayout For Dynamic Cell Heights as well as an open source component to help make this more abstract and easier to implement. https://github.com/Raizlabs/RZCellSizeManager

share|improve this answer
 
up vote3down vote

Like @Bob-Spryn I ran into an important enough gotcha that I'm posting this as an answer.

I struggled with @smileyborg's answer for a while. The gotcha that I ran into is if you've defined your prototype cell in IB with additional elements (UILabelsUIButtons, etc.) in IB when you instantiate the cell with [[YourTableViewCellClass alloc] init] it will not instantiate all the other elements within that cell unless you've written code to do that. (I had a similar experience with initWithStyle.)

To have the storyboard instantiate all the additional elements obtain your cell with [tableView dequeueReusableCellWithIdentifier:@"DoseNeeded"] (Not [tableView dequeueReusableCellWithIdentifier:forIndexPath:] as this'll cause interesting problems.) When you do this all the elements you defined in IB will be instantiated.

share|improve this answer
 
up vote2down vote

As long as your layout in your cell is good.

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {    UITableViewCell *cell = [self tableView:tableView cellForRowAtIndexPath:indexPath];    return [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;}
share|improve this answer
 
 
This is working for me on iOS7, is it now OK to call tableView:cellForRowAtIndexPath: in tableView:heightForRowAtIndexPath: now? –  Ants Jun 8 at 8:37
 
Ok so this is not working, but when I call systemLayoutSizeFittingSize: in tableView:cellForRowAtIndexPath: and cache the result then and then use that in tableView:heightForRowAtIndexPath: it works well as long as the constraints are setup correctly of course! –  Ants Jun 8 at 9:21
up vote1down vote

The answer by wildmonkey is great.

I was solving the same problem for a couple of weeks and just to share my solution here too:https://github.com/kuchumovn/wheely-ios-test/blob/master/wheely-test/DynamicRowHeightTableViewController.m

You can download the whole app and test it in XCode https://github.com/kuchumovn/wheely-ios-test/

Thoroughly tested and works in any orientation, and even has caching implemented.

  • Attention: you need to use MultilineLabel instead of UILabel because UILabel has a sizing bug; see https://github.com/kuchumovn/wheely-ios-test/blob/master/wheely-test/extension/MultilineLabel.m
share|improve this answer
 
up vote1down vote

Dynamic Table View Cell Height and Auto Layout - http://www.raywenderlich.com/73602/dynamic-table-view-cell-height-auto-layout

A good way to solve the problem with storyboard Auto Layout:

- (CGFloat)heightForImageCellAtIndexPath:(NSIndexPath *)indexPath {  static RWImageCell *sizingCell = nil;  static dispatch_once_t onceToken;  dispatch_once(&onceToken, ^{    sizingCell = [self.tableView dequeueReusableCellWithIdentifier:RWImageCellIdentifier];  });  [sizingCell setNeedsLayout];  [sizingCell layoutIfNeeded];  CGSize size = [sizingCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];  return size.height;}
share|improve this answer
 
1 
This has already been extensively covered in the accepted answer to this question. –  smileyborg Aug 22 at 14:44
1 
Yes, I know... but I didn't want to use PureLayout and the dispatch_once 'trick' helped me a lot, to work it out only using the Storyboard. –  Mohnasm Aug 22 at 21:30
up vote0down vote

While all of these solutions are great, they all include calling -layoutIfNeeded or -setNeedsLayout at some point to force a drawing cycle of the off-screen cell for calculating it's height. Even if you cache the height, and you had 500 different cells to display, that's still drawing the cell off-screen 500 times, then using that data to draw the cell on-screen again another 500 times. It's just not that efficient in my opinion. 

I recently came up with a solution that doesn't require drawing off-screen other than setting text on a UILabel, so I thought I would share.

Here's a sample project w/ helper classes, but I'll explain the differences here too: https://github.com/henrytkirk/HTKDynamicResizingCell

How to avoid the need to force off-screen cell drawing:

  1. Define a default size for cells: Use that to set preferredMaxLayoutWidth on any multi-line labels when you setup constraints. The default height will end up being more of a minimum height if for some reason the cell calculated to be too small.

  2. Constrain image width/height: This can be a big gotcha - you need to make sure you either constrain the width and height of an UIImageView or use a properly sized image. Remember, when auto layout asks an imageView for it's size when it has no w/h constraints (it's intrinsicContentSize), it will return the 1x size of that image. So if you use a 200x200px image that you mean't to display in a UIImageView sized 75x75pt, without any constraints, the intrinsic content size would return 200x200.

Tip for UITableViewCells:

If you are using a UITableViewCell that has an accessoryType other than UITableViewCellAccessoryNone, you need to factor that width into the preferredMaxLayoutWidth of your multi-line labels. I've measured it to be 33pt in size. Since you won't have the cell's contentView frame until it's drawn, you need to account for this since you aren't actually drawing the cell.

Hope this is helpful. Sorry for posting on an old topic.

share|improve this answer
 
 
Performing a layout pass (setNeedsLayout) is different than drawing/rendering a view (setNeedsDisplay) -- when you use an offscreen cell, it is never drawn. Your solution isn't any different than what has already been discussed in this thread...perhaps you don't realize that calling systemLayoutSizeFittingSize: on a view implies and requires a layout pass to solve the constraints.–  smileyborg 5 hours ago
up vote-2down vote

I tried many solutions, but the one that worked was this, suggested by a friend:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {    int height = [StringUtils findHeightForText:yourLabel havingWidth:yourWidth andFont:[UIFont systemFontOfSize:17.0f]];    height += [StringUtils findHeightForText:yourOtherLabel havingWidth:yourWidth andFont:[UIFont systemFontOfSize:14.0f]];    return height + CELL_SIZE_WITHOUT_LABELS; //important to know the size of your custom cell without the height of the variable labels}

The StringUtils.h class:

#import <Foundation/Foundation.h>@interface StringUtils : NSObject+ (CGFloat)findHeightForText:(NSString *)text havingWidth:(CGFloat)widthValue andFont:(UIFont *)font;@end

StringUtils.m class:

#import "StringUtils.h"@implementation StringUtils+ (CGFloat)findHeightForText:(NSString *)text havingWidth:(CGFloat)widthValue andFont:(UIFont *)font {    CGFloat result = font.pointSize+4;    if (text) {        CGSize size;        CGRect frame = [text boundingRectWithSize:CGSizeMake(widthValue, CGFLOAT_MAX)                                          options:NSStringDrawingUsesLineFragmentOrigin                                       attributes:@{NSFontAttributeName:font}                                          context:nil];        size = CGSizeMake(frame.size.width, frame.size.height+1);        result = MAX(size.height, result); //At least one row    }    return result;}@end

It worked perfectly for me. I had a Custom Cell with 3 images with fixed sizes, 2 labels with fixed sizes and 2 variable labels. Worked like a charm. Hope it works for you too. 

The solution presented by smileyborg naturally is more complete. But if you are looking for something simpler, this might suit you.

Best regards, Alexandre.

share|improve this answer
 
 
will it also work for different orientation...where the width will change? –  harshitgupta May 13 at 16:26
 
I cant find any framework for StringUtils. From where are you referencing it? –  harshitgupta May 13 at 16:35
 
Hello. I do "#import "StringUtils.h" in the TableViewController.  Regarding the layout, my project is for iPhone in portrait orientation, so I have a fixed width of 320.  Later I can edit my answer so it deal with that, but for now, I think you could try setting:  havingWidth:[tableView.frame.size.width]                 –                      Alexandre                 May 13 at 16:54                                                                             
 
I need the file "StringUtils.h" and "StringUtils.m" in order to import them? –  harshitgupta May 13 at 17:15
3 
This answer doesn't address the question, which specifically asks how to use Auto Layout to determine the row height. This answer doesn't rely on Auto Layout at all, in fact -- it requires you to do the math yourself. – smileyborg May 14 at 16:20
up vote-4down vote

in iOS8 Self-Sizing Cells feature has been introduced. I have provided a tutorial about it also explained what happens underhood. It is pretty simple and will boost your development time!

https://github.com/kkocabiyik/SelfSizingCellExample


转载:http://stackoverflow.com/questions/18746929/using-auto-layout-in-uitableview-for-dynamic-cell-layouts-variable-row-heights

0 0
原创粉丝点击