Dynamically-sized UITableView in UIScrollView with Auto-Layout - ios

There are a million questions about auto-layout and UIScrollViews, but I think this is something different.
I have a construct that looks like this:
The white background is a UIView which acts as a container within a UIScrollView (the typical setup for get a scroll view to build its own content size so it scrolls properly based on the container view).
TableView 1 and TableView 2 need to have their heights be dynamic. I don't want them scrolling, so I need to set their frame height equal to their content height.
I have IBOutlets for the height constraint for each of these, and can adjust them with no problem.
The issue arises when I try to adjust the height of the container view. Ultimately, I would like to not have to mess with it and let that view resize automatically based on its content, but that does not seem to be working (if I leave off the height of the container view, I get warnings/errors in IB about the constraints).
If I create an outlet to the container view's height constraint and set it, I get an assert fail:
*** Assertion failure in -[UIView layoutSublayersOfLayer:], /SourceCache/UIKit_Sim/UIKit-2935.137/UIView.m:8803
I am doing the constraint adjusting in viewDidLayoutSubviews like this:
- (void)viewDidLayoutSubviews {
self.orderItemTableViewHeightConstraint.constant = self.orderItemTableView.contentSize.height - [self tableView:self.orderItemTableView heightForHeaderInSection:0];
self.shippingOptionTableViewHeightConstraint.constant = self.shippingMethodTableView.contentSize.height;
self.scrollViewContainerViewHeightConstraint.constant = self.cardInfoLabel.$bottom + 60;
}
I believe the problem is setting the container view's height constraint is causing some recursion or something...
The questions:
IS there a way to let the container view's height be fluid based on its content in this situation?
If not, how do I handle this programmatically? Am I altering the constraints in the wrong place or something?
Thanks for any input!

Ok, well, just shotgunning code here, but after much trial and error, this seems to resolve the assert failure and gives the desired result:
- (void)viewDidLayoutSubviews {
self.orderItemTableViewHeightConstraint.constant = self.orderItemTableView.contentSize.height - [self tableView:self.orderItemTableView heightForHeaderInSection:0];
self.shippingOptionTableViewHeightConstraint.constant = self.shippingMethodTableView.contentSize.height;
[self.view layoutIfNeeded];
self.scrollViewContainerViewHeightConstraint.constant = self.cardInfoLabel.$bottom + 60;
[self.view layoutIfNeeded];
}
Would really like to have this more dynamic, but will take the win at this point in the game!
Hope this helps someone else.

Related

UIView frame not updating in viewDidAppear

I'm using Storyboard with autolayout. In my ViewController I have a basic structure: one UIScrollView and one contentView (UIView) that contains different labels). Then I have different elements that I add in the viewDidAppear method of the class inside my contentView , then in order to recalculate it's frame size I do this:
- (void)fixContentViewHeight
{
_contentView.frame = CGRectMake(_contentView.frame.origin.x, _contentView.frame.origin.y, _contentView.frame.size.width, operators.frame.origin.y + operators.frame.size.height);
//constraint for contentView height
_contentViewHeight.constant = operators.frame.origin.y + operators.frame.size.height;
}
- (void)fixScrollViewHeight
{
_scrollView.frame = _contentView.frame;
_scrollView.contentSize = _contentView.frame.size;
}
where operators is the LAST placed element of contentView, it's always on the bottom of the frame. I call these 2 methods inside the viewDidAppear and they make the view scrollable, the problem is that the frame of contentView doesn't get updated so the last elements are always unclickable (because they're placed outside the frame of the view).
What am I doing wrong? Why the scrollView becomes scrollable but the contentView keeps it's old frame?
If you have autolayout constraints affecting the height of _contentView You will not be able to change its height by setting the frame, as the constraints will override that.
You will need to (and should be) adding new / modifying constraints in your code when you are adding new elements. then calling
_contentView.layoutIfNeeded()
After all the updates to let the UI update if it needs to, which it should.
Please update UI On Main Queue
dispatch_async(dispatch_get_main_queue(), ^{
// update any UI on Main queue
});
I think that all your frame related information should be inside viewWillLayoutSubviews() and viewDidLayoutSubviews(). You can add stuff in viewDidLoad() but for any frame management I would use mentioned methods.
In your particular situation use former, viewDidLayoutSubviews(). However, you can resolve these kind of issues using constraints connected to the code as #Simon McLoughlin mentioned. 'scrollView becomes unscrollable' means that you should update contentSize as well so keep an eye on that as well.
Try to add vertical space constraint between scroll view and your last placed element.

UIScrollview: What is setting the contentSize back to 0?

I have a UIScrollView, and in viewDidAppear I set the contentSize:
- (void)viewDidAppear:(BOOL)animated{
CGSize scrollContentSize = CGSizeMake(320, 9200);
self.scrollView.contentSize = scrollContentSize;
}
This code is definitely running.
However, the view doesn't scroll. I wired up a button to log the contentSize, and it returns 0,0. If I get the button to set contentSize again it works fine.
I'm not referencing scrollView anywhere else in my code, what could be setting the contentSize back to 0, and is there any way I can stop it from doing so, or run my setup later in the process of setting up the view?
if you used the Autolayout then you have to make constraints for subviews in it. Then Autolayout will calculate its content size automatically.
But when I set contentsize through button action it works? How?
When the view relayout the scrollview again it will be calculated based on the constraints. So it won't work when u rotate the device/relayout the view.
I don't want to use the autolayout?Now what?
Then you are good to set contentsize.
One more thing, it is best practice to call super implementation on few lifecyle methods.
so do it so.
- (void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
// Your code here
}
Here is the Good starting point
Excellent tutorial

UITableView Header Gap and Resizing Programmatically

I have two questions related to UITableViews.
1) The first one is, what is the gap at the top of the UITableView? My app always starts with the top cell not flush with the top of the tableview (as shown in the second image), it starts about one cell lower, i.e. where that gap is in the interface builder. I can't find where that is coming from, or why.
2) I can't seem to resize the uitableview programmatically, I'm trying to reduce the height after a popup appears. I've shown an example of it not working in the second picture.
Here is (an example of) what I am trying at the moment:
- (void)viewDidLoad
{
[super viewDidLoad];
self.table_view.delegate = self;
CGRect tableBounds = self.table_view.bounds;
tableBounds.size.height -= 100;
self.table_view.bounds = tableBounds;
CGRect tableFrame = self.table_view.frame;
tableBounds.size.height -= 100;
self.table_view.frame = tableFrame;
}
Thanks!
UITableView Selected:
Simulation:
In your xib (or storyboard) put your UITableView at position (0,0). ( the same position as the navigation bar).
The first image shows that your table view has problems even in Interface Builder. It looks as if you've set the top inset incorrectly; check your edge insets.
The reason your resizing code is not working is probably that it is too early (viewDidLoad); put it in viewDidAppear: and see if that works, and if it does, try moving it back to viewWillAppear: so the user doesn't see the resizing. If it doesn't work, it might be because you're using auto layout; you can't manually alter the frame of something whose frame is dictated by auto layout. (Your resizing code is also silly; you want to set the frame, not the bounds.) But it might also be because you're using a UITableViewController in a UINavigationController; if you do that, the table view is under the navigation controller's direct control and its size is not up to you.

UIView with dynamic height that uses intrinsicContentSize

I am trying to create a custom container view that has a UIImageView and a multiline UILabel as subviews. To make the view work nicely with autolayout, I am overriding intrinsicContentSize as below:
- (CGSize)intrinsicContentSize
{
return [self sizeThatFits:self.bounds.size];
}
The size calculated in sizeThatFits has the same width, and adjusts the height so that the label and image are not clipped. This works well, but I was surprised to see in the docs the following comment:
This intrinsic size must be independent of the content frame, because there’s no way to dynamically communicate a changed width to the layout system based on a changed height, for example.
If that is the case, what is the autolayout way to adjust the views current height based on its width and content? Should I be approaching this in a different way?
To answer my own question, it appears that there is not an autolayout suitable solution to this situation. Looking to UILabel for inspiration, the problem here has been solved with the addition of a property preferredMaxLayoutWidth, which can then be used as a constraining width during the intrinsic content size calculation. Any custom view would need to use something similar.
I think the doc means that, your containerView might have a placeHolderFrame as content frame.
intrinsic size should not be related to the content frame, but only to it's own subContent.
Your image and UILabel for example.
You should calculate both height and the width from the label and the image.
Which should be easy, since they all have intrinsic size.
Just my opinion...
I guess you could use UILabel's new preferredMaxLayoutWidth property to layout label correctly and use other approaches to layout other stuff.
Something like this:
- (void)layoutSubviews
{
...
[super layoutSubviews]; // get width from solved constraints
label.preferredMaxLayoutWidth = label.frame.size.width; // use it
[super layoutSubviews]; // update height of a label (probably intrinsicContentSize)
...
}
Align the bottom edge of the containing view with both the image and the label.
[self alignBottomEdgeWithView:labelView predicate:#"10"];
Details in http://code.dblock.org/ios-uiview-with-an-image-and-text-with-dynamic-height.

How do I get auto layout to reposition a view when it's sibling view changes size?

I have a view controller with two views (greenView, redView) and a switch:
I have a constraint on the redView top space of 50px from greenView. Here are all the constraints on redView:
I have a switch that when tapped toggles the height of greenView from 50px to 100px.
When I launch the app, the views are laid out as I want, but when the switch is tapped, the greenView frame changes from a height of 50 to 100, the redView doesn't do what I would expect - which is to shift it's y position 50px down to maintain the top space constraint it was assigned. I have an inequality constraint put on the bottom space of redView so there are no conflicts, and I am also calling [self.redView layoutIfNeeded].
Here's the relevant code:
- (IBAction)switchTapped {
if (theSwitch.isOn) {
greenView.frame = CGRectMake(0, 30, 320, 100);
[redView layoutIfNeeded];
} else {
greenView.frame = CGRectMake(0, 30, 320, 50);
[redView layoutIfNeeded];
}
Why isn't redView's y position being updated?
I have watched WWDC videos, but they seem to mention calling layoutIfNeeded in passing. What am I missing?
Here's what it looks like when launched:
And here it is when the switch is tapped:
A couple of pointers:
layoutIfNeeded assumes that you have previously called setNeedsLayout. Use setNeedsLayout instead and your app will update the layout as soon as appropriate.
Setting a frame generally does not work with autolayout. Set or change a height constraint instead. This is something you should do in code, mros had a good suggestion for this.
I find that inequality constraints are usually not the best solution, since they often lead to ambiguous layout. Instead, set an equality constraint, but lower the priority (e.g. to 500). The autolayout system will try to honor the constraint as much as possible, but will prioritize other constraints. (By default, all constraints have 1000 priority.)
There are a couple things that are causing problems. The first thing is that autolayout is supposed to ignore the object's frame. If you want to change the height of something, you change its height constraint, not the frame height. This is because constraints are applied after your layout and would override any frame changes. In addition, the red view looks like it is constrained to your superview "view" not "greenView". As far as editing the constraints, you can set up an IBOutlet and then modify the constant property of the UILayoutConstraint object.

Resources