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.
Related
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).
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;
}
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
}
I am trying to update the frame of a UIView which contains buttons and labels inside. I am trying to update it in viewDidLayoutSubviews (and I also tried in viewDidLoad, viewWillAppear, viewDidAppear..). I want to change the y position (origin.y) of the view.
The NSLogs says my original y position is 334, and after changing, it is 100. However, the position does not change in my view. I have already checked that the view is connected in the storyboard. What am I doing wrong?
-(void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
CGRect theFrame = [self.bottomView frame];
NSLog(#"Y position bottomview: %f", self.bottomView.frame.origin.y);
if([[UIScreen mainScreen] bounds].size.height == 568) //iPhone 4inch
{
// NSLog(#"iphone5");
}
else{
// NSLog(#"iphone4");
theFrame .origin.y = 100;
}
self.bottomView.frame = theFrame;
NSLog(#"Y position bottomview after changing it: %f", self.bottomView.frame.origin.y);
[self.view layoutIfNeeded];
}
I've had the same problem. Forcing the layouting for your view's superview helped me out:
-(void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
[self.bottomView.superview setNeedsLayout];
[self.bottomView.superview layoutIfNeeded];
// Now modify bottomView's frame here
}
In Swift:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
bottomView.superview!.setNeedsLayout()
bottomView.superview!.layoutIfNeeded()
// Now modify bottomView's frame here
}
Believe it or not the below code fixed it
override func viewDidLayoutSubviews() {
DispatchQueue.main.async {
// UI changes
}
}
The problem was related with Autolayout. However I couldn't turn it off since I am using autolayout in my project. I solved defining appropriate constraints in the view. Then there is no need to check if it is iPhone 4inch or 3.5inch and change the position of the frame since it automatically adapts to each size.
The frame setting should work in your code. But if the view has autolayout constraints (which I assume you have), your frame setting won't work. You can only go one way or the other (manual frame setting or autolayout), not both.
I want to add a view to the bottom of the content view of both a collection view and table view (and hence is applicable to any kind of scroll view) and I also want to be able to scroll down to see this view e.g.:
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
// Observe change in content size so can move my view when
// content size changes (keep it at the bottom)
[self addObserver:self forKeyPath:#"contentSize"
options:(NSKeyValueObservingOptionPrior)
context:nil];
CGRect frame = CGRectMake(0, 0, 200, 30);
self.loadingView = [[UIView alloc] initWithFrame:frame];
[self.loadingView setBackgroundColor:[UIColor blackColor]];
[self addSubview:self.loadingView];
// Increase height of content view so that can scroll to my view.
self.contentSize = CGSizeMake(self.contentSize.width, self.contentSize.height+30);
}
return self;
}
However when, for example, a cell is inserted the contentSize is recalculated and whilst my view is still visible at the bottom of the content size (due to being able to bounce the scroll view) I can no longer scroll to it.
How do I ensure that the content size stays, as in my code, 30 points taller?
An additional question is:
is there any other way to track content size other than observing it?
Thanks in advance.
I have tried:
- (void)layoutSubviews
{
[super layoutSubviews];
self.contentSize = CGSizeMake(self.contentSize.width, self.contentSize.height+30);
}
However this causes all sorts of display issues.
If i understand correctly, you want to show a loading view in the tableView (f.e.) at the bottom. You could add an extra UITableViewCell containing this LoaderView to the tableView.
(Must change the numberOfRowsInTableView)
In another perspective for scrollViews: Use smaller bounds then the content itself, to make it scrollable. For example frame = fullscreen. At every cell adding or modification in subviews (adding) contentSize = content size + 30 px.
Try making a subclass of the scroll view and override the contentSize getter to return always 30 px more.
- (CGSize)contentSize {
CGSize customContentSize = super.contentSize;
customContentSize.height += 30;
return customContentSize;
}
(I'm writing the code by memory, there may be errors)