intrinsicContentSize causes infinite loop in special case - ios

I made a container view i call SimpleStackView. The idea is simple, any subviews are stacked on top of eachother. The width of a SimpleStackView determines the width of its subviews, and the height of a SimpleStackView is determined by the height of its subviews.
I do it in layoutSubviews where i call sizeThatFits on each subview and layout them on top of eachother using the returned heights. The sum of those heights also determine what is returned from both the sizeThatFits override and intrinsicContentSize override of SimpleStackView.
I support iOS 7 so i cant use UIStackView.
I use AutoLayout to layout most things in my app. My SimpleStackView works fine in many places where its laid out using AutoLayout next to other views (i rely on its intrinsicContentSize to define its height, no height constraints), except in one case where a SimpleStackView is put in the contentView of a UITableViewCell in a UITableView. In that one case, an infinite loop is triggered. Im not an AutoLayout guru. I might be missing something about how intrinsicContetSizes are used inside AutoLayout? What could be the case of this? How do i use intrinsicContentSize properly so it works correctly in all cases?
The code of SimpleStackView is relatively short; here's the full class implementation:
#implementation SimpleStackView
#synthesize rowSpacing=_rowSpacing;
- (void)layoutSubviews
{
[super layoutSubviews];
[self sizeToFit];
[self invalidateIntrinsicContentSize];
CGFloat nextRowTop = 0;
for (UIView *view in self.subviews)
{
CGSize size = [view sizeThatFits:CGSizeMake(self.bounds.size.width, view.bounds.size.height)];
view.frame = CGRectMake(0, nextRowTop, self.bounds.size.width, size.height);
nextRowTop += view.frame.size.height + self.rowSpacing;
}
}
- (CGSize)sizeThatFits:(CGSize)size
{
CGFloat sumOfHeights = 0;
for (UIView *view in self.subviews) {
sumOfHeights += [view sizeThatFits:CGSizeMake(size.width, view.bounds.size.height)].height;
}
CGFloat sumOfRowSpacings = MAX(0, (int)self.subviews.count - 1) * self.rowSpacing;
return CGSizeMake(size.width, sumOfHeights + sumOfRowSpacings);
}
- (CGSize)intrinsicContentSize
{
CGFloat intrinsicHeight = [self sizeThatFits:self.bounds.size].height;
return CGSizeMake(UIViewNoIntrinsicMetric, intrinsicHeight);
}
// i tried this to fix the infinite loop; didnt work was still stuck in infinite loop
//- (void)setFrame:(CGRect)frame
//{
// CGRect frameBefore = self.frame;
// [super setFrame:frame];
// if (NO == CGRectEqualToRect(frameBefore, frame))
// [self invalidateIntrinsicContentSize];
//}
#end
edit: I forgot to mention; the UITableCellView that causes the infinite loop has an unbroken chain of constraints from the top of contentView to bottom of contentView. The infinite loop stops happening when i remove one of the constraints to break the chain. I'd like to keep the constraints, they are there to compress a multiline UILabel when row height is small (which is set in the UITableViewDelegate's heightForRowAtIndexPath).

Related

view's frame don't change

I have a tableview.when reload data I will configure cell and update constraint. after that cell'slayoutSubviews be invoked,but view's frame can't be update
- (void)configModel:(LKSportActivityEventModel *)model {
//some code before
CGFloat titleWidth = 100;
[self.activityNameView mas_updateConstraints:^(MASConstraintMaker *make) {
make.width.mas_equalTo(titleWidth);
}];
//some code after
}
- (void)layoutSubviews {
[super layoutSubviews];
[self setCorner:UIRectCornerBottomRight | UIRectCornerTopLeft
bounds:self.activityNameView.bounds
cornerSize:CGSizeMake(4, 4)
targetView:self.activityNameView
layer:self.activityNameViewShapLayer];
}
activityNameView's bounds don't change
The default implementation uses any constraints you have set to determine the size and position of any subviews.
layoutSubviews only computes the size and position of its subviews. It does not lay out the whole view subtree.
After UITableViewCell.layoutSubviews, only the content view's frame is right.
The easier way to fix your layout is to call contentView.layoutIfNeeded inside layoutSubviews.

layoutIfNeeded behavior change iOS8 to iOS9

For certain cases with AutoLayout I need to know the width of my view (most nested subview) within it's superview. With AutoLayout in iOS 8 I was able to rely on layoutIfNeeded for the layout system to layout the frames and get the proper width before I do this calculation.
An example would be something like this:
- (CGSize)intrinsicContentSize {
[self layoutIfNeeded];
CGSize size = [self roundedSizeAccountingLeftRightInsets:CGSizeMake(self.bounds.size.width, CGFLOAT_MAX)];
size.height += self.insets.top + self.insets.bottom;
return size;
}
This no longer works with iOS 9. I'm sure that all constraints to be able to calculate the width are set (usually just leading, trailing constraints bound to the superview).
I noticed this in the release notes for iOS 9 but I wasn't really able to interpret it.
In iOS 9, when layoutIfNeeded is sent to a view and all of the following conditions are satisfied (which is not common), we apply fitting-size constraints (width/height = 0 at UILayoutPriorityFittingSizeLevel) instead of required size constraints (width/height required to match current size):
The receiver is not yet in the subtree of a view that hosts a layout engine, such as window, view controller view (unless you have set translatesAutoresizingMaskIntoConstraints to NO on that view—or created constraints that have one item in its subtree and one item outside it), table view cell content view, and so on.
The final ancestor (that is, top-level view) of the receiver has translatesAutoresizingMaskIntoConstraints set to NO.
The top-level view has a subview that is not a UIViewController-owned layout guide that also has translatesAutoresizingMaskIntoConstraints set to NO.
Under condition 1, we create a temporary layout engine from the top-level view and add all the constraints from the subtree to it. The problem is that we need to add some constraints that make the size of the top-level view unambiguous in the layout engine. The old behavior (prior to iOS 9) was that we would add constraints to restrict the size of the top-level view to its current bounds for any situation under condition 1. This really doesn’t make sense when you add conditions 2 and 3 and can result in unsatisfiable-constraints logging and broken layout.
So in iOS 9, for this special case only, we use fitting-size constraints instead.
This means that if you are sending layoutIfNeeded to a view under these conditions in iOS 9, you must be sure that either you have sufficient constraints to establish a size for the top-level view (which usually, though not always, is the receiver) or you must add temporary size constraints to the top-level view of layout size you desire before sending layoutIfNeeded, and remove them afterward.
Has anyone else encountered this issue, or familiar with how to solve?
Edit: Couple More Examples
I usually do this when I need to know explicitly what the layout width will be of the superview because constraints of the subview are dependent on this value and can't be expressed with preferredMaxLayoutWidth.
The first example is a custom view with an array of labels. When constraints are updated I need to know the width so I can know if those labels will continue on the same line or move down to the next line.
- (void)updateConstraints {
[self layoutIfNeeded];
CGFloat width = self.view.bounds.size.width;
for (UILabel *label in self.labels) {
CGSize labelSize = [label sizeThatFits:CGSizeZero];
CGFloat minLabelWidth = MAX(12, labelSize.width);
labelSize.width = minLabelWidth;
lineWidth += labelSize.width + 10;
if (lineWidth >= width) {
// update some variables to where I will actually be applying constraints
}
[label mas_updateConstraints:^(MASConstraintMaker *make) {
// constraint magic
}];
[super updateConstraints];
}
One more:
In this example there will sometimes be a text label that is shown based on a condition. If it needs to be shown I expand it to it's appropriate height constrained to the width of it's superview (it only has insets to it's leading and trailing superview). If it doesn't need to be shown I collapse the label.
- (void)updateConstraints {
// Need layout pass to get the proper width.
[self layoutIfNeeded];
CGFloat textHeight = [self.label sizeThatFits:CGSizeMake(self.bounds.size.width - 32, CGFLOAT_MAX)].height;
[self.label mas_remakeConstraints:^(MASConstraintMaker *make) {
// update other constraints
make.height.equalTo( showThisText ? #(textHeight) : #0 );
}];
[super updateConstraints];
}
There can also be a case when I need a textField to be shown and not be pushed off the screen by other elements along the x axis so I have to give it a fixed width via constraints but I need to know the max width before I do that
- (void)updateConstraints {
[self layoutIfNeeded];
CGFloat textFieldWidth = self.bounds.size.width - someVariable;
[self.textField mas_remakeConstraints:^(MASConstraintMaker *make) {
make.width.equalTo(#(textFieldWidth));
}];
[super updateConstraints];
}
I ended up overriding layoutSubviews since this is a UIView subclass and it seems to be working now with this code
- (void)layoutSubviews {
[super layoutSubviews];
static CGSize viewBounds = { 0, 0 };
static CGSize previousViewBounds = { 0, 0 };
[self setNeedsUpdateConstraints];
viewBounds = CGSizeMake(self.bounds.size.width, self.bounds.size.height);
if (!CGSizeEqualToSize(viewBounds, previousViewBounds)) [self setNeedsUpdateConstraints];
previousViewBounds = viewBounds;
}

Dynamically resize UICollectionView's supplementary view (containing multiline UILabel's)

Inside a UICollectionView's supplementary view (header), I have a multiline label that I want to truncate to 3 lines.
When the user taps anywhere on the header (supplementary) view, I want to switch the UILabel to 0 lines so all text displays, and grow the collectionView's supplementary view's height accordingly (preferably animated). Here's what happens after you tap the header:
Here's my code so far:
// MyHeaderReusableView.m
// my gesture recognizer's action
- (IBAction)onHeaderTap:(UITapGestureRecognizer *)sender
{
self.listIntro.numberOfLines = 0;
// force -layoutSubviews to run again
[self setNeedsLayout];
[self layoutIfNeeded];
}
- (void)layoutSubviews
{
[super layoutSubviews];
self.listTitle.preferredMaxLayoutWidth = self.listTitle.frame.size.width;
self.listIntro.preferredMaxLayoutWidth = self.listIntro.frame.size.width;
[self layoutIfNeeded];
CGFloat height = [self systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
self.frame = ({
CGRect headerFrame = self.frame;
headerFrame.size.height = height;
headerFrame;
});
NSLog(#"height: %#", #(height));
}
When I log height at the end of layoutSubviews, its value is 149 while the label is truncated and numberOfLines is set to 3. After tapping the headerView, setting numberOfLines to 0, and forcing a layout pass, height then gets recorded as 163.5. Great!
The only problem is that the entire headerView doesn't grow, and the cells don't get pushed down.
How can I dynamically change the height of my collectionView's supplementary view (preferably animated)?
I'm aware of UICollectionViewFlowLayout's headerReferenceSize and collectionView:layout:referenceSizeForHeaderInSection: but not quite sure how I'd use them in this situation.
I got something working, but I'll admit, it feels kludgy. I feel like this could be accomplished with the standard CollectionView (and associated elements) API + hooking into standard layout/display invalidation, but I just couldn't get it working.
The only thing that would resize my headerView was setting my collection view's flow layout's headerReferenceSize. Unfortunately, I can't access my collection view or it's flow layout from my instance of UICollectionReusableView, so I had to create a delegate method to pass the correct height back.
Here's what I have now:
// in MyHeaderReusableView.m
//
// my UITapGestureRecognizer's action
- (IBAction)onHeaderTap:(UITapGestureRecognizer *)sender
{
self.listIntro.numberOfLines = 0;
}
- (void)layoutSubviews
{
[super layoutSubviews];
self.listTitle.preferredMaxLayoutWidth = self.listTitle.frame.size.width;
self.listIntro.preferredMaxLayoutWidth = self.listIntro.frame.size.width;
CGFloat height = [self systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
self.frame = ({
CGRect headerFrame = self.frame;
headerFrame.size.height = height;
headerFrame;
});
if (self.resizeDelegate) {
[self.resizeDelegate wanderlistDetailHeaderDidResize:self.frame.size];
}
}
// in my viewController subclass which owns the UICollectionView:
- (void)wanderlistDetailHeaderDidResize:(CGSize)newSize
{
UICollectionViewFlowLayout *flowLayout = (UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout;
// this is the key line
flowLayout.headerReferenceSize = newSize;
// this doesn't look beautiful but it's the best i can do for now. I would love for just the bottom of the frame to animate down, but instead, all the contents in the header (the top labels) have a crossfade effect applied.
[UIView animateWithDuration:0.3 animations:^{
[self.collectionView layoutIfNeeded];
}];
}
Like I said, not the solution I was looking for, but a working solution nonetheless.
I ran into the same issue than you, so I was just wondering: did you ever get a solution without the crossfade effect that you mention in the code sample?. My approach was pretty much the same, so I get the same problem. One additional comment though: I managed to implement the solution without the need for delegation: What I did was from "MyHeaderReusableView.m" You can reference the UICollectionView (and therefore, the UICollectionViewLayout) by:
//from MyHeaderReusableView.m
if ([self.superview isKindOfClass:UICollectionView.class]) {
//get collectionView reference
UICollectionView * collectionView = (UICollectionView*)self.superview;
//layout
UICollectionViewFlowLayout * layout = (UICollectionViewFlowLayout *)collectionView.collectionViewLayout;
//... perform the header size change
}

Resizing the content UIView in a UIScrollView, is there a good way to do this without having to do any calculations?

I have a UIScrollView, the frame size is that of the screen. Inside the scrollview is my content UIView. This view has a number of buttons in it. These are dynamic and the number will grow over time so I need the content UIView's size to also be increased. Is there a good way to get the UIView to resize to fit around all the subviews inside (i.e the UIButtons)?
I tried this method:
[self.content sizeToFit];
self.scrollView.contentSize=self.content.frame.size;
Doesn't seem to be doing anything though.
The default implementation of sizeThatFits: on UIView doesn't affect any change when using sizeToFit. Instead, you could subclass UIView and use this heavy-handed approach:
#interface MyView : UIView
#end
#implementation MyView
- (CGSize)sizeThatFits:(CGSize)size {
CGSize contentSize = CGSizeZero;
for (UIView *subview in [self subviews]) {
contentSize.width = MAX(contentSize.width, CGRectGetMaxX(subview.frame));
contentSize.height = MAX(contentSize.height, CGRectGetMaxY(subview.frame));
}
size.width = MIN(size.width, contentSize.width);
size.height = MIN(size.height, contentSize.height);
return size;
}
#end

How to set the width of a cell in a UITableView in grouped style

I have been working on this for about 2 days, so i thought i share my learnings with you.
The question is: Is it possible to make the width of a cell in a grouped UITableView smaller?
The answer is: No.
But there are two ways you can get around this problem.
Solution #1: A thinner table
It is possible to change the frame of the tableView, so that the table will be smaller. This will result in UITableView rendering the cell inside with the reduced width.
A solution for this can look like this:
-(void)viewWillAppear:(BOOL)animated
{
CGFloat tableBorderLeft = 20;
CGFloat tableBorderRight = 20;
CGRect tableRect = self.view.frame;
tableRect.origin.x += tableBorderLeft; // make the table begin a few pixels right from its origin
tableRect.size.width -= tableBorderLeft + tableBorderRight; // reduce the width of the table
tableView.frame = tableRect;
}
Solution #2: Having cells rendered by images
This solution is described here: http://cocoawithlove.com/2009/04/easy-custom-uitableview-drawing.html
I hope this information is helpful to you. It took me about 2 days to try a lot of possibilities. This is what was left.
A better and cleaner way to achieve this is subclassing UITableViewCell and overriding its -setFrame: method like this:
- (void)setFrame:(CGRect)frame {
frame.origin.x += inset;
frame.size.width -= 2 * inset;
[super setFrame:frame];
}
Why is it better? Because the other two are worse.
Adjust table view width in -viewWillAppear:
First of all, this is unreliable, the superview or parent view controller may adjust table view frame further after -viewWillAppear: is called. Of course, you can subclass and override -setFrame: for your UITableView just like what I do here for UITableViewCells. However, subclassing UITableViewCells is a much common, light, and Apple way.
Secondly, if your UITableView have backgroundView, you don't want its backgroundView be narrowed down together. Keeping backgroundView width while narrow down UITableView width is not trivial work, not to mention that expanding subviews beyond its superview is not a very elegant thing to do in the first place.
Custom cell rendering to fake a narrower width
To do this, you have to prepare special background images with horizontal margins, and you have to layout subviews of cells yourself to accommodate the margins.
In comparison, if you simply adjust the width of the whole cell, autoresizing will do all the works for you.
To do this in Swift, which does not provide methods to set variables, you'll have to override the setter for frame. Originally posted (at least where I found it) here
override var frame: CGRect {
get {
return super.frame
}
set (newFrame) {
let inset: CGFloat = 15
var frame = newFrame
frame.origin.x += inset
frame.size.width -= 2 * inset
super.frame = frame
}
}
If nothing works you can try this
Make the background colour of the cell as clear color and then put an image of the cell with required size. If you want to display some text on that cell put a label above the image. Don't forget to set the background color of the label also to clear color.
I found the accepted solution didn't work upon rotation. To achieve UITableViewCells with fixed widths & flexible margins I just adapted the above solution to the following:
- (void)setFrame:(CGRect)frame {
if (self.superview) {
float cellWidth = 500.0;
frame.origin.x = (self.superview.frame.size.width - cellWidth) / 2;
frame.size.width = cellWidth;
}
[super setFrame:frame];
}
The method gets called whenever the device rotates, so the cells will always be centered.
There is a method that is called when the screen is rotated : viewWillTransitionToSize
This is where you should resize the frame. See example. Change the frame coords as you need to.
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
[coordinator animateAlongsideTransition:nil completion:^(id<UIViewControllerTransitionCoordinatorContext> context)
{
int x = 0;
int y = 0;
self.tableView.frame = CGRectMake(x, y, 320, self.tableView.frame.size.height);
}];
}
i do it in
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell;
CGFloat tableBorderLeft = self.view.frame.origin.x + 10;
CGFloat tableBorderRight = self.view.frame.size.width - 20;
CGRect tableRect = self.view.frame;
tableRect.origin.x = tableBorderLeft;
tableRect.size.width = tableBorderRight;
tableView.frame = tableRect;
}
And this worked for me
In .h file add the delegate 'UITableViewDataSource'
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return size;
}

Resources