Auto-sizing UITableViewCell in iOS 8 - ios

I have a UITableViewCell subclass which contains a multiline label, and I would like the cell to size itself dynamically based on the content of that label. I'm aware that iOS 8 introduced auto-sizing cells based on AutoLayout constraints, and I've found several examples of this already on SO, but I'm still having some trouble implementing this behavior properly.
Here's my updateConstraints implementation:
- (void)updateConstraints {
[super updateConstraints];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-10-[_nameLabel(==20)]-10-[_tweetLabel]" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_nameLabel, _tweetLabel)]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:[_avatarView]-10-[_nameLabel]-10-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_avatarView, _nameLabel)]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[_nameLabel]-10-[_tweetLabel]-10-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_nameLabel, _tweetLabel)]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:[_avatarView]-10-[_tweetLabel]-10-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_avatarView, _tweetLabel)]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-10-[_avatarView(==45)]" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_avatarView)]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-10-[_avatarView(==45)]" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_avatarView)]];
}
In the table view controller I set the row height to UITableViewAutomaticDimension (and I set an estimated row height as well). At runtime, I get a series of auto layout errors and all of the table view cells appear nearly completely overlapped.
The auto layout conflicts are between the following constraints:
V:|-(10)-[_nameLabel]
V:[_nameLabel(20)]
V:[_nameLabel]-(10)-[_tweetLabel]
V:[_tweetLabel]-(10)-|
V:[cell(44)]
I suspect the last constraint, "UIView-Encapsulated-Layout-Height", which forces a height of 44, is the cause of the issue, but I'm not quite sure where that comes from, so hopefully somebody can shed some light on the issue.

In order to implement automatic row heights for table view cells, you need to do the following:
Implement Auto Layout constraints within the cell's contentView that allow the view to express its preferred height. Be sure to set UILabels to word wrap over multiple lines.
Be sure you've defined an axial chain of constraints in both dimensions, that is, constraints that collectively bind all the way from one edge of the view to the other. Perhaps the easiest way to be sure these constraints are correct is to implement your custom content as a plain old UIView (which is easy to test), and then use constraints so that the UITableViewCell.contentView hugs that view. (I use this gist to automate building the "view-wrapping cell".)
Set tableView.rowHeight = UITableViewAutomaticDimension
Set tableView.estimatedRowHeight = 400 or some other reasonably generous value, in order to workaround some UIKit bugs when the estimate is too low.
I have spent a puzzling amount of time working with this feature. This github repo shows seven complete examples of self-sizing table view cells containing a single label of wrapping text -- programmatic, nib-based, storyboard-based, etc..
Finally, do not worry too much if you see warnings about unsatisfiable constraints mentioning "UIView-Encapsulated-Layout-Height" or similar at the first time the table view loads. This is an artefact of UITableView's initial process for creating a cell, determining what its size should be based on Auto Layout Constraints, and keeping the UITableViewCell tightly wrapping its contentView. The repo I mentioned above has more extensive discussion and code for exploring this somewhat awkward corner of the API.
You should only worry about constraint-violation warnings if they persist even after the cell has loaded and has scrolled a bit, or if you are seeing incorrect layouts initially. In this case, again, the first step should always be to ensure your constraints are correct by developing them and testing them in isolation if possible, in a plain UIView.

I just came across this issue.
From numerous other Stackoverflow post they recommend:
self.contentView.autoresizingMask = UIViewAutoresizingFlexibleHeight;
That didn't work for me at first. I found that I also need to do:
self.frame = CGRectMake(0, 0, self.frame.size.width, 50);
My custom cell's init method looks like his:
-(id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if(self)
{
[self initViews];
[self initConstraints];
}
return self;
}
I put the code in my "initViews" method:
-(void)initViews
{
...
// fixes an iOS 8 issue with UIViewEncapsulated height 44 bug
self.contentView.autoresizingMask = UIViewAutoresizingFlexibleHeight;
self.frame = CGRectMake(0, 0, self.frame.size.width, 50);
}
The problem went away and my cell looks correct too.
Does this work for you?

Are you sure you have -translatesAutoresizingMaskIntoConstraints set to NO on the cell? If you don't, the system generates constraints based on the autoresizing mask, which was the previous way of doing layout on iOS, and should be disabled when using Auto Layout.

Related

Superview not increasing in height based on the subviews constraint

I have a scrollview and a separate UIView where I placed a series of textFields and labels with constraints which fully occupies the top and bottom. I'm trying to adjust the UIView's height based on its subview constraints but it won't. What is happening is that the view keeps its height and force other textfields to collapse or shrink thus breaking the constraints.
Details
Each subview priority values :
compression = 750
hugging = 250
UIView priority values:
compression = 249
hugging = 749 Set to be lower than the rest.
Most of the textfields has aspect ratio constraint. This causes the field to adjust.
Each subview has vertical/top/bottom spacing between each other. The top and bottom elements has top and bottom constraints to the view as well.
What's on my code:
-(void)viewDidLayoutSubviews{
[super viewDidLayoutSubviews];
/* I had to adjust the UIView's width to fill the entire self.view.*/
if(![contentView isDescendantOfView:detailsScrollView]){
CGRect r = contentView.frame;
r.size.width = self.view.frame.size.width;
contentView.frame = r;
[detailsScrollView addSubview:contentView];
}
}
Screenshots
The view
This is what currently happens. In this instance it forces the email field to shrink. If I place a height value on it, it does not shrink but the layout engine finds another element to break
Edit:
Solved
Maybe I just needed some break to freshen up a bit. I did tried using constraints before but got no luck. However thanks to the suggestion I went back setting the constraints instead of setting the frame on this one and got it finally working.
Solution:
-(void)viewDidLoad{
[detailsScrollView addSubview:contentView];
[contentView setTranslatesAutoresizingMaskIntoConstraints:NO];
[detailsScrollView setTranslatesAutoresizingMaskIntoConstraints:NO];
NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(contentView,detailsScrollView);
NSArray *horizontalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-0-[contentView]-0-|"
options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil
views:viewsDictionary];
NSArray *verticalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-0-[contentView]-0-|"
options:NSLayoutFormatDirectionLeadingToTrailing
metrics:nil
views:viewsDictionary];
NSArray *widthConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-0-[contentView(==detailsScrollView)]-0-|" options:0 metrics:nil views:viewsDictionary];
}
When you use interface builder to deal with the UIScrollView and its child UIView. usually a top, bottom, left and equal width constraints are set between the UIScrollView and its child which is the contentView in your case.
Without those constraints the other option is to set the content size of the UIScrollView. which was the way of using the UIScrollView before introducing constraints.
So, 1. you should add those constraints programmatically.
By using the constraints, the views frame is no longer needed to resize the views.
So, 2. remove frame setting for your content view.
I am not so happy with the way you set the frame in the viewDidLayoutMethod. if I am going to do that here I would take the frame setting out of the if statement.
The code would be as follow with no if statement:
[detailsScrollView addSubview:contentView];
// then set the constraints here after adding the subview.
Put this code anywhere but not inside your viewDidLayoutSubviews method. it will be a bigger problem than setting the frame in there inside if statement.
Note: Originally, if you are going to set frame in the viewDidLayoutSubviews
method. you should do it for all cases. for example for the if case
and the else case. because, next time this method is going to be
called the views will respond to the constraint. and lose its frame.
Another observation: if you want the view to response to its subviews constraint why you need to set the frame for it? right?
After adding the constraint you may need to call the method constraintNeedsUpdate or another related method.

UICollectionView cell height/scrollable content behaving strange/incorrect

I'm trying to make UICollectionView work like UITableView (I want the extra flexibility of collection view instead of just going with table view, both for some current and possible future feature extensions); having a fixed width (screen width) and dynamic cell height (just like iOS 8's new table view feature). I've been struggling a lot to get it working. First, I've enabled automatic sizing by setting an estimated size on layout on my collection view subclass:
UICollectionViewFlowLayout *layout = (UICollectionViewFlowLayout*)self.collectionViewLayout;
layout.estimatedItemSize = CGSizeMake(SCREEN_WIDTH, 100);
Then I've set up my constraints of my custom cell in Interface Builder to let it grow according to my text view's content.
I've also set up a width constraint equal to screen width programatically, otherwise my cells were having a default width of 50pt. Inside my custom cell's awakeFromNib:
[self addConstraint: [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:SCREEN_WIDTH]];
It seems to work for width. When I run my app, it breaks height constraints as my own constraints conflict with UIView-Encapsulated-Layout-Height which is set to 100 (this actually means that auto-height is not really working well).
I lower the priority from my vertical constraints, especially the one with textview (which grows depending on its content) from required (1000) to some lower number (900). I'm not getting broken constraints anymore, but that's because we don't need to break constraints anymore as UIView-Encapsulated-Layout-Height takes precedence. Result is exactly the same. Here is what I'm getting:
There is no problem with the GIF above. It really does jump at one point exactly as seen on the GIF.
How can I prevent this behavior and make my collection view cells grow in height dynamically (of course, without creating a dummy offscreen cell to calculate view sizes)?
I don't need to support iOS 7, so iOS 8-and-above-only solutions are welcome.
UPDATE: If I implement preferredLayoutAttributesFittingAttributes: method as seen on the answer to UICollectionView Self Sizing Cells with Auto Layout:
-(UICollectionViewLayoutAttributes *)preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes{
UICollectionViewLayoutAttributes *attrs = [layoutAttributes copy];
CGRect frame = attrs.frame;
self.frame = frame;
[self setNeedsLayout];
[self layoutIfNeeded];
float desiredHeight = [self.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
frame.size.height = desiredHeight;
attrs.frame = frame;
return attrs;
}
Then it's always returning the NIB's original view size that was in the Interface Builder even though I've set:
self.translatesAutoresizingMaskIntoConstraints = NO;
self.contentView.translatesAutoresizingMaskIntoConstraints = NO;
I have no idea why it's returning an incorrect value.
After spending hours and days, I've decided to go back to table view. While collection view offers great flexibility and is definitely the future, it's just not there yet.

Programatically creating a UIView with multiple Labels

Xcode Interface Builder issue
Personally I do not like the way that interface builder works in Xcode. In this example I am trying to create a fairly complex view controller. On the viewDidLoad of the view controller I show a custom alert view (as such). It is not actually an alert view but more of a view that shows the user some information. I have a dimmed background view and a view on top of this. If I try to create this in interface builder it gets overly complicated as you cannot select the views in the background and move them etc without dropping subviews into the wrong views and so on...
Scenario
What I am trying to do is create a View which holds some labels and a button. The view controller has a difficulty property based on this it will have different text in the labels/amount of labels.
I.e. Easy -- 3 labels
Hard -- 4 labels
I create the dimmedView and alert(styled)View like this:
// Setup the dimmedView
UIView *dimmedView = [[UIView alloc] initWithFrame:self.view.frame];
dimmedView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.6];
// Setup the startingAlertView
UIView *startingAlertView = [[UIView alloc] init];
startingAlertView.backgroundColor = [UIColor whiteColor];
I then create the three/four labels based on some logic and add the necassary labels to the startingAlertView based on logic also.
The issue that is obvious is that at no point a frame for the view is set. This means that it is returning 0,0,0,0. What I would like to happen is the view to take the required height based on the labels added.
I am building for IOS7 and using Auto Layout. Should I be setting up constraints which would then adjust the relevant heights and locations in the view possibly?
I am building for IOS7 and using Auto Layout. Should I be setting up constraints which would then adjust the relevant heights and locations in the view possibly?
Yes. you don't use initWithFrame: under auto layout, or rather, you can, but the frame is ignored. Create your dimming view with a frame of CGRectZero, setting translatesAutoresizingMasksToConstraints to NO, add it to your main view and create constraints pinning it to all edges of the superview.
Then, add your alert view, again with a frame of zero and the translates... property set to NO. Create constraints to centre this view in your dimming view. This view will get its size from its subviews, since labels have an intrinsic size.
Add your labels as subviews of this view, with frame of zero and translates... set to NO. Depending on their content you may wish to set preferred max layout width or a width constraint.
Create constraints pinning your labels to the left and right edges of the superview, and lining your labels up in a vertical 'stack'. In each case you could add padding to give your alert a bit of a border.
This can look like a large amount of code, so you may want to read the articles I've written on visual format for auto layout and creating constraints in code, with the associated autolayout convenience category to make your life easier.
If you're going to the auto layout route, then you can add constraints that will keep the proper space between each label, and the proper space between the top and bottom of the view with the first and last labels. However, if you're not doing this in Interface Builder, you might as well skip using auto layout also, because it's fairly simple to just adjust the height of the view as you add labels.
You would start by setting the height of the view to the size of the top and bottom spaces that you want to have around the labels. Then each time you add a label, add to it the height of the label plus the height of the space you're putting between labels.
You could also wait until you've added all of the labels that you want, then set the height to the bottom label's y position plus its height plus the bottom space you want to have around the labels.
Yes, using autolayout you can get the bounds from the parent view.
Here is a quick example, notice that we are not using frame, and using CGRectZero for our UILabels, the positioning comes from updateConstraints instead. I am using Visual Format Language to layout the labels which I recommend if you are doing it programatically.
Here we are making the labels the width of the parent view and then just stacked on top of each other.
#import "View.h"
#implementation View{
UILabel *_label1;
UILabel *_label2;
UILabel *_label3;
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
_label1 = [[UILabel alloc] initWithFrame:CGRectZero];
_label1.translatesAutoresizingMaskIntoConstraints = NO;
_label1.text = #"LABEL 1";
_label2 = [[UILabel alloc] initWithFrame:CGRectZero];
_label2.translatesAutoresizingMaskIntoConstraints = NO;
_label2.text = #"LABEL 2";
_label3 = [[UILabel alloc] initWithFrame:CGRectZero];
_label3.translatesAutoresizingMaskIntoConstraints = NO;
_label3.text = #"LABEL 3";
[self addSubview:_label1];
[self addSubview:_label2];
[self addSubview:_label3];
}
[self updateConstraintsIfNeeded];
return self;
}
-(void)updateConstraints
{
[super updateConstraints];
NSDictionary *_viewsDictionary = NSDictionaryOfVariableBindings(_label1,_label2,_label3);
// Set the contraintsto span the entire width of the super view
NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[_label1]-|"
options:0
metrics:nil
views:_viewsDictionary];
[self addConstraints:constraints];
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[_label2]-|"
options:0
metrics:nil
views:_viewsDictionary];
[self addConstraints:constraints];
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[_label3]-|"
options:0
metrics:nil
views:_viewsDictionary];
[self addConstraints:constraints];
// Last setup the vertical contraints other wise they will end up in a random place
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[_label1]-[_label2]-[_label3]"
options:0
metrics:nil
views:_viewsDictionary];
[self addConstraints:constraints];
}
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect
{
// Drawing code
}
*/
#end

UITableViewCell cell reuse and UILabel auto-layout dynamic sizing

I'm working with a subclassed UITableViewCell and I need to align a UILabel's text to the Top Left all while using Auto Layout. I realize reading up that sizeToFit really shouldn't be used with Auto Layout and I'd like to avoid it, and somehow use a constraint. Basically the label's text is reset every reuse of the cell so the sizing would need to be dynamic on reuse.
Here's the Lazy initializer label inside the subclassed cell:
- (UILabel *)commentsLabel {
if (_commentsLabel == nil) {
_commentsLabel = [[UILabel alloc] init];
[_commentsLabel setTranslatesAutoresizingMaskIntoConstraints:NO];
_commentsLabel.numberOfLines = 0;
_commentsLabel.lineBreakMode = NSLineBreakByWordWrapping;
_commentsLabel.preferredMaxLayoutWidth = 100;
}
return _commentsLabel;
}
There are auto-layout constraints being set on the label (commentsLabel is a subView added to self.customView on the cell subclass):
[self.customView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-locationToTopPadding-[locationLabel(locationHeight)]-locationToCommentsPadding-[commentsLabel]-commentsToBottomPadding-|"
options:0
metrics:#{
#"locationToTopPadding":#(locationToTopPadding),
#"locationHeight":#(locationHeight),
#"locationToCommentsPadding":#(locationToCommentsPadding),
#"commentsToBottomPadding":#(commentsToBottomPadding)
}
views:viewsDictionary]];
[self.customView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:[thumbnailImageView]-[commentsLabel]-|"
options:0
metrics:#{#"commentsWidth":#(commentsWidth)}
views:viewsDictionary]];
setting just:
self.commentsLabel.preferredMaxLayoutWidth = 100;
doesn't seem to work although that is mentioned in most answers.
currently have this implemented but not seeing any results.
UILabel sizeToFit doesn't work with autolayout ios6
another answer I tried.
UILabel sizeToFit only works with AutoLayout turned off
I feel that I'm just missing 1 constraint that can be added in addition to the constraint above but the programmatic constraints I try to add don't work or throw an exception. I'm working completely in code there are no xibs.
ETA: tried setting a height constraint inside the existing vertical constraint line:
like suggested here: Dynamically change UILabel width not work with autolayout
in the vertical constraint line above making it:
-[commentsLabel(>=30#900)]-
I've messed around with the height value and priority value and nothing changed.
ETA: making some progress, I think it has to do with the bottom padding of the label, tried this and some of the labels are aligned correctly some aren't:
->=commentsToBottomPadding-
solution in the event any one else runs into the same issue with a UITableViewCell and a custom Label that changes dynamically:
-[commentsLabel]->=yourBottomPadding-
put that in the vertical constraint
call this in the viewController after you set the text:
[cell updateConstraints];

iOS AutoLayout not really working for views embedded into ScrollView

I'd like to embed views into each other (in the current case into a scrollview to allow displaying taller content than the screen's size) and use iOS 6.0's AutoLayout feature in order to avoid constant calculation of content size's.
I have the following ViewController's view, containing a UIScrollView:
I'd like to display my ChildVC's view in this scrollview:
Please notice that the label is multiline and contains a 'lot of' text, also auto-layout constraints are defined (escpecially with a greater-than-or-equal to the height property).
I create the childVC and add it to the main VC's view via the following code:
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
childVC = [[ChildVC alloc] initWithNibName:#"ChildVC" bundle:nil];
self.myScrollView.translatesAutoresizingMaskIntoConstraints = NO;
childVC.view.translatesAutoresizingMaskIntoConstraints = NO;
[self.myScrollView addSubview:childVC.view];
NSDictionary *viewsDictionary = #{ #"subView" : childVC.view};
NSString* constr = [NSString stringWithFormat:#"|-0-[subView(%f)]-0-|", self.myScrollView.frame.size.width];
[self.myScrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:constr options:0 metrics: 0 views:viewsDictionary]];
[self.myScrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-0-[subView]-(>=0)-|" options:0 metrics: 0 views:viewsDictionary]];
}
The constraints for the content-view (which I guess will be the childVC's view) are the only stuff set up in code, because I only want a vertical scrollbar, I want the child-view's content to (horizontally) shrink to be displayed in the scrollviewer.
And my output is this:
The UI is displayed, shrinked but my label does not resize vertically therefore it does not display the whole text :(
I tried to set up translatesAutoresizingMaskIntoConstraints to the label here and there without any success.
I'd appreciate any help because I'm struggling with this for days now :/
Personally, I think those constraints and the auto layout feature just don't always work exactly as we want them to. (Especially on different screen sizes).
So I normally add a few line of code in the viewDidLoad method to move certain objects around by a few pixels. You can also change sizes and so on.
Literally a simple movement like this does the trick without the user noticing any animations:
object_name.frame = CGRectOffset(object_name.frame, 0, -40.0f);

Resources