Autolayout how to get computed value from view - ios

There is one thing that is really making me crazy about auto layout , I'm doing my tests and found that if you subclass a UIView and you put some views with their constraints is impossible to know the computed value.
Let't say that we have a TestViewClass that inherits from UIView, we've got some subviews inside. we want to use it for iPhone and iPad, so we use in two sizes, let's suppose 100x100 and 200x200 we made our constraints to make everything work. We build this view placing those subviews in some positions.
Now we need to build another subviews that contains a number of buttons as subviews (contentView) which number is given at runtime.
The algorithm will pick the size of this content view and put the correct number of buttons calculating the space between them in a way that the will be at the same distance from each other but covering the whole content view width. For example, the contentView is 200 point in width, buttons are 3 and squared with a side of 60. Together they cover 180, so we have 20 points left that should be placed as a space between the buttons->10 points.
That's pretty easy, made thousand of times before auto layout. To do that I need the width of the contentView which has some constraints to its superview that make it resize its width according to superview size, the problem is that I can't find any place inside UIView implementation where I can get the final value of the size of the contentView.
I tried view -layoutSubviews, -updateConstraints, -didMoveToSuperview, value show are alway the original one. When the new frames are calculated?
I clearly didn't get something about auto layout...
I discovered this problem trying to set table view cell height, trying to make them appear all on screen independently by the size of the table view. here is the related question other question

Finally I've get what I was missing about auto layout and it is a fundamental concept.
Autolayout doesn't work like autoresizing masks, the new frames aren't
updated instantaneously but by request.
This makes a huge difference, it seems to be in sync with the CATransaction render cycle, that makes sense, because layout calculation could be an expensive task and is useless to do until you really need it, most of the time during rendering.
Of course there are few exceptions, like this one and how can we force auto layout to make this calculation? we can do setting -setNeedsLayout method and -layoutIfNeeded. the first method marks the view as "dirty" and the second forces an immediate layout. If you set call those two methods in -didMoveToSuperview right after you get correct updated frames. Hope this helps,
Andrea

You can use view controller's viewDidLayoutSubviews, which will notify you when the views are laid out. If all of this is adding of constraints is happening in a custom UIView, you could simply write a method for your view called viewDidLayoutSubviews and have the view controller call that when the view controller receives it. At this point, the containers should have their dimensions properly configured.
But, there are a couple of approaches of even spacing a bunch of buttons without needing to bother knowing the size of the container view:
One approach I've used for even spacing controls in a container is to create what I call "spacer" views (views that are present, but just have a clearColor background, so you can't visually see them). You can then create a series of constraints using, effectively, something like:
H:|[spacer1][button1(60)][spacer2(==spacer1)][button2(60)][spacer3(==spacer1)][button3(60)][spacer4(==spacer1)]|
If your number of buttons is fixed, you can do this in a single constraintsWithVisualFormat, like above. If it's a variable number of buttons, you can iterate through them, building a VFL string for each pair of button and spacer (e.g. the first button would use VFL of
H:|[spacer][button(60)]
all the subsequent ones would create another button and another spacer and use the following VFL:
H:[previousButton][spacer(==originalSpacer)][button(60)]
and then I add one final spacer at the end:
H:[lastButton][spacer(==originalSpacer)]|
This is a little cumbersome, but as you can see, your controls are perfectly laid out, evenly spaced, and you never needed to know the dimensions of the container.
Another approach is to use the NSLayoutFormatAlignAllCenterX, attribute. This bypasses the need to create all of those spacers, but if you use a simple algorithm like I do below, the centers will be evenly distributed amongst themselves. Thus, in addition to the standard vertical constraints and the horizontal width constraint for the button, you can then control the horizontal placement of the buttons with:
[containerView addConstraint:[NSLayoutConstraint constraintWithItem:button[i]
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:containerView
attribute:NSLayoutAttributeCenterX
multiplier:2.0 * (double) (i + 0.5) / (double) n
constant:0.0]];
where i is the zero-based index of which button you're setting this horizontal constraint for, and n is the number of buttons.

Related

How do I prepare a UIViewController's view for being added to a UIStackView?

This is what I'm trying to do...
I have one view controller that needs to dynamically display different subviews based on the presence of some data.
Here is a simple mockup. Each colored block represents a unique subview.
Sometimes the green block needs to be at the top, sometimes the green block won't display at all, sometimes the light blue block will be something different, etc.
Each subview has interactive elements, so I've been creating and adding them like so:
Defining a new view controller
Defining its view
Calling addChildViewController and didMoveToParentViewController
Calling addSubview on myNewViewController.view
Using SnapKit to make auto layout constraints to position the view
I want to transition to UIStackView because it seems a good support system for this view because all I need to do is stack its subviews. I'm seeing many conflicting constraint errors and unexpected view frames when trying to add subviews with their own inner auto layout constraints.
Question
Am I setting myself up for failure here by embedding the views of 4-6 view controllers in the view of one view controller?
Also, how do I give the added views properties like minimum heights or content sizes without seeing many breaking constraints with UIStackView? (So they can stack, but one of them is say, 400 tall, and the other is 200 tall)
Answer
You can absolutely do this using UIContainerViews combined with UIStackViews and UIScrollViews, it's a complicated setup so here's a starter project to get you started:
https://github.com/Rnorback/ScrollingStackView
Here's what that project looks like:
You want the containers to have different sizes. In order to do that, simply add height constraints to the containers with Autolayout. If you want to change the height of the scrolling. Then you'll need to change the height constraint of the UIStackView.
Brief Explanation
When creating this setup, you have to make sure the UIStackView distribution setting stays in fill. That way you can set height constraints on UIContainerViews.
When setting up anything in a UIScrollView you have to make sure that the object is pinned to the edges of the scroll view and has a defined width and height, otherwise the scrollview will throw a constriant error. I think of it like the scrollview tries to press in on all sides of your content view and if any side gives, the scrollview won't be able to present it properly.
Hope this helps.

Temporarily remove NSLayoutConstraint?

I have a UITableViewCell subclass laid out using AutoLayout, and I’d like a small animation in which the image view transforms, grows a bit and shrinks back.
At present, due to the constraints, the labels on the right side of the image are moving too (as they should be).
I’d like a quick and easy means to temporarily say “leave those labels where they currently are while this animation is running”. Is that something I can do without needing to remove and re-add those constraints, which is a lot of hassle?
Starting in iOS 8 NSLayoutConstraints now have an active property that you can change myConstraint.active = NO;. This removes the constraint, while setting this value to YES will [re]add it:
Activating or deactivating the constraint calls addConstraint: and
removeConstraint: on the view that is the closest common ancestor of
the items managed by this constraint. Use this property instead of
calling addConstraint: or removeConstraint: directly.
source
Additionally, there are two new class methods activateConstraints: and deactivateConstraints: that take an array of constraints (NSArray<NSLayoutConstraint>), so that can be helpful, especially if you have an IBOutletCollection with constraints.
[NSLayoutConstraint deactivateConstraints:myConstraints];
There's no API to say "leave these views here", temporarily disabling some parts of the autolayout. There are several possible solutions:
remove your constraints during animation
arrange your constraints differently (making them to a superview of the image view, for example, or having them only depend on the center of the image view)
override layoutSubviews of a container view to stick your labels back where you want them after autolayout runs in [super layoutSubviews] (or remove the transform of your image view, run autolayout and then put it back)
subclass your image view and override frameForAlignmentRect: and alignmentRectForFrame: to get autolayout to use the untransformed frame for alignment purposes.
More details and suggestions are available in the answers here, which is a similar question of how to make transforms and autolayout play together: How do I adjust the anchor point of a CALayer, when Auto Layout is being used?

resize superview after subviews change dynamically using autolayout

I cant for the love of god the the hang of this resizing superview.
I have a UIView *superview with 4 UILabels. 2 function as header for the 2 others.
The content in all 4 are dynamic coming from database.
SizeToFit vs SizeThatFits:(CGSize) vs UIView systemLayoutSizeFittingSize:, passing either UILayoutFittingCompressedSize or UILayoutFittingExpandedSize.
I use autolayout programatically and have set the superview height to be equal or greater to a dummy number.
where and how do I use these SizeToFit vs sizeThatFits:(CGSize) vs UIView systemLayoutSizeFittingSize:, passing either UILayoutFittingCompressedSize or UILayoutFittingExpandedSize. I have read a lot of tips here on stack but ended up with nothing.
DO I need to recalculate the constraints for the superview somewhere specific. Maby setting the height to be ´#property` in its controller class and remove and readd it?
Atm I have tried to put everything everywhere and then some. Still I get the same size end result with the dummy height and text floating outside. Even after setting clipsToBound on subview.
I am scratching my hair of.. help
If you're using Auto Layout, here's what you need to do:
Make sure you aren't adding fixed width and/or height constraints to any of your subviews (depending on which dimension(s) you want to dynamically size). The idea is to let the intrinsic content size of each subview determine the subview's height. UILabels come with 4 automatic implicit constraints which will (with less than Required priority) attempt to keep the label's frame at the exact size required to fit all the text inside.
Make sure that the edges of each label are connected rigidly (with Required priority constraints) to the edges of each other and their superview. You want to make sure that if you imagine one of the labels growing in size, this would force the other labels to make room for it and most importantly force the superview to expand as well.
Only add constraints to the superview to set its position, not size (at least, not for the dimension(s) you want it to size dynamically). Remember that if you set the internal constraints up correctly, its size will be determined by the sizes of all the subviews, since its edges are connected to theirs in some fashion.
That's it. You don't need to call sizeToFit or systemLayoutSizeFittingSize: to get this to work, just load your views and set the text and that should be it. The system layout engine will do the calculations for you to solve your constraints. (If anything, you might need to call setNeedsLayout on the superview...but this shouldn't be required.)
Use container views
In the following example I have a 30x30 image, and the UILabel is smaller than the containing view with the placeholder text. I needed the containing view to be at least as big as the image, but it needed to grow to contain multi-line text.
In visual format the inner container looks like this:
H:|-(15.0)-[image(30.0)]-(15.0)-[label]-(15.0)-|
V:|[image(30.0)]|
V:|[label(>=30.0)]|
Then, set the containing view to match the height of the label. Now the containing view will ride the size of the label.
As #smileyborg pointed out in his answer, connecting the content rigidly to the superview informs the layout engine that the simple container view should cause it to grow.
Yellow alignment rectangles
If you want the yellow alignment rectangles add -UIViewShowAlignmentRects YES in your scheme's list of run arguments.
This almost follows #smileyborg answer and comes with a concrete example.
Won't describe all constraints, but those related to the calculation of the height of UI objects.
[Label] Labels must not have a fixed height constraint, in this case, AutoLayout won't resize labels to fit the text, so setting edge constraints is the key. (green arrows)
[Subview] Steps 1 and 3 are very easy to follow, but this step can be misunderstood. As in the case with labels, subviews must not have height constraint set. All subviews must have top constraint set, ignoring bottom constraint, which can make you think will trigger unsatisfied constraint exception at runtime, but it won't if you set bottom constraint for the last subview. Missing to do so will blow the layout. (red arrows)
[Superview] Set all constraints the way you need, but pay big attention to the
height constraint. Assign it a random value, but make it optional, AutoLayout will set the height exactly to fit the subviews. (blue arrows)
This works perfectly, there is no need to call any additional system-layout update methods.
This was made dramatically easier with the introduction of Stack Views in iOS 9. Use a stack view inside your view to contain all your content that resizes, and then simply call
view.setNeedsUpdateConstraints()
view.updateConstraintsIfNeeded()
view.setNeedsLayout()
view.layoutIfNeeded()
after changing your content. Then you can get your new size by calling
view.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
if you ever need to calculate the exact size required for a view.

Why is my UIButton ignoring its autolayout constraint to stretch its height?

I have a button whose image is set dynamically at run time. As a result, to maintain the appropriate ratio, I have the following code in where the image is set:
[super removeConstraint:self.ratioConstraint];
float ratio=photo.size.height/photo.size.width;
self.ratioConstraint=[NSLayoutConstraint constraintWithItem:self.button
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:self.button
attribute:NSLayoutAttributeWidth
multiplier:ratio
constant:0];
[self.ratioConstraint setPriority:UILayoutPriorityDefaultHigh];
[super addConstraint:self.ratioConstraint];
I even applied a [self layoutSubviews] to let me check the frame. When I queried the frame, it was right. The ratio of 1 was honored and the height was set equal to the width.
But in ViewWillAppear, the same query produces the same impossible results: the height of the frame is stretching to let the image grow to it's full height. Despite having the above constraint, which should limit the height based on the existing (and honored) width limits.
Even more infuriating, until I set the image to something new at runtime (i. e. by taking the camera), the restraint appears to be functional -- the placeholder image is squashed down horizontally, and compacts itself vertically to fit. It's only once I take a photo that that height explodes, ignoring it's constraint for no apparent reason.
In case it matters, the above code is executed in a custom subclass of UIControl, which is then embedded in a UIView ('ContentView') which is itself embedded in a scroll view. The width restrictions are, more or less:
"ContentView" has a width equal to the ViewController's view.
The UIControl view then has it's width set to a value that either gives it about half or about one quarter of the screen's width, depending on the exact control. (I have five of them; one gets half the screen width, the others all get one quarter and are arranged in a row).
The UIControl then sets the UIButton's width equal to it's own via the constraint: H:|-(0)-[Button]-(0)-| .
Even more annoyingly, I ran through and inspected every constraint from the UIViewController's view down -- a bit of recursive logic that pulled up every constraint in a given view and it's subviews that applied to a given subject. I then ran that against each object in the view hierarchy, and got... zip. No constraints that were interfering. Nothing that should have effected the height of hte buttons in question except the code above, which apply the ratio constraint.
The solution actually proved to be rather embarrassingly obvious, though I'm not sure I understand why the solution worked (or the code above didn't).
In the code above, I used the constant 'UILayoutPriorityDefaultHigh'; in my init code, I used the constant 'UILayoutPriorityRequired'.
Apparently, the photo's desire to stretch out to it's full size is a high priority, so I could only override that 'silent' constraint by upping my own to required. (Interestingly, the bug actually existed before I added the priority calls at all, suggesting that a constraint doesn't start out with that priority -- something I suspect may confuse more than one programmer out there when they trip over it).

iOS Autolayout intrinsicContentSize = -1

When using autolayout, calling intrinsicContentSize seems to be the method to determine what CGSize is required to properly fit the views content.
However, this method is only supported for a limited number of existing UIViews.
Anytime that I make a custom view, even if it is something as simple as a UILabel inside of a container UIView, that containing view is unable to determine its intrinsicContentSize (returns -1).
I don't understand why the view is able to properly be displayed on the screen yet the view doesn't even know its own height...
The UILabel in a container view is a simple example but I'm dealing with slightly more complicated UIViews where there are maybe 15 views nested within eachother. In order to determine the size of the view which contains all of its subviews, I have to manually create my own intrinsicContentSize method and do very time consuming work where I have to sum up all the heights of the subviews plus add to that all of the constraints.
This process is terrible. It's very easy to miss out on a height somewhere by forgetting to add the height of one of the subviews or constraints. Also, the matter is further complicated by the fact that with dynamic subviews. For example, if the view has 2 columns of dynamic subviews, you need to manually find the height of the subviews+constraints for each column, compare these heights and return the larger of the two. Again, this is a simple example but often it's not so simple, causing many many migraines.
Back to what I was asking earlier. How can iOS display the view yet not even know how tall the view is? There must be some way to find out what that height is. Thanks for reading.
Here is an image to help visualize what I want.
Are all your subviews using auto-layout themselves? I mean that if your using auto-layout to place MyCompositeObject, is that composite object using constraints internally to place its many objects? I've found that if so, then the intrinsicContentSize will account for all the subviews, but if not, your UIView's intrinsic content size is going to end out returning something inaccurate and small.

Resources