AutoLayout: UIView within UIView has incorrect frame - ios

I am working with AutoLayout in Storyboard, and everything seemed to be going fine. However, when I put one UIView within another and apply all the constraints I want to both the container and its children, I notice that the frame is incorrect in viewDidLayoutSubviews:. Rather than being the frame I would expect to be calculated given my constraints, it is some disgustingly large frame (albeit at the correct origin). For example, rather than a frame of {{26, 10}, {444, 10}} I get something like {{0, 0}, {320, 568}}.
Strangely enough, this only happens to the child UIView when I situate it within another UIView that has some constraints applied to it in relation to its superview (which is the view controller's view). As I am under the impression that I can expect my views to be properly laid out according to my constraints in the method viewDidLayoutSubviews:, I am confused as to why this is happening.
Am I making any incorrect assumptions? I would appreciate it if someone might help point me in the right direction. Thanks!
Note: This problem is completely fixed if I do my correct-frame-dependent setup in viewDidAppear:, but this is a somewhat unsatisfying workaround for me.

Auto Layout changes to bounds are not necessarily finished in -viewDidLayoutSubviews. As put in "Advance Auto Layout Toolbox":
Constraint-based layout is an iterative process. The layout pass can
make changes to the constraints based on the previous layout solution,
which again triggers updating the constraints following another layout
pass.
The documentation for -viewDidLayoutSubviews notes with emphasis in the original:
However, this method being called does not indicate that the individual layouts of the view's subviews have been adjusted.
So layout and constraint-based iterations in the subviews could continue to adjust their frames, but if a ViewController's view's bounds don't change, its -viewWillLayoutSubviews won't get called.
Since you say your setup is "frame-dependent" you have a couple options.
1. Do your setup in -viewWillAppear. Better than -viewDidAppear, and since you really need everything to be done after all the layout if completed but before it appears on screen, this is a legitimate reason why the method is there. If relevant, you could try calling -isMovingToParentViewController
as described in Determining Why A View's Appearance Changed.
EDIT: -viewWillAppear is called before the view is added to the view hierarchy and therefor layout may not be completed. Sorry everyone, and thankfully with Swift UI we are moving away from procedural layout to declarative layout, and this is why.
Call -layoutIfNeeded on the views you need to have the correct values where you need them to. Because this does the layout work it's expensive, and it could lead to repetitive work being done or even an infinite loop if triggered during its own layout process. But it's useful if it's needed to sync some values with Auto Layout so they can be used.
If this doesn't work let us know, it could have to do with the nature of the constraints themselves or something else. The Auto Layout iterations can be tricky to sync with. Good luck.

Related

If a view is set up in IB with Auto Layout, what happens if you try to change its frame programmatically?

A project I've started helping on did not use Auto Layout before and I'm updating it to use Auto Layout and size classes. There's a decent amount of frame manipulation code throughout the app (e.g. setting frame directly rather than changing constraint constants), and I'm wondering how this affects a view that's been set up with Auto Layout constraints.
I'm working on doing away with the frame-changing portions of code and changing it to update constraint constants where needed, but since I'm not yet 100% familiar with how every piece of the code works, it'd be helpful to have a better understanding of how auto layout and coded frame changes can affect each other so that if a view doesn't appear properly at runtime I can better determine if it's something I set up or perhaps a piece of older code somewhere that needs to be found and updated.
It's very simple. You just have to understand what auto layout is.
Here's how it works. The constraints are just a list of instructions; they do not, of themselves, actually do anything at all. There's a system message layoutSubviews, which is sent at moments you do not control — so you should imagine it could be sent any time. When layoutSubviews is actually sent, the constraints are consulted and obeyed (by doing exactly what you would do — that is, the runtime sets the frame, or the bounds and center, of each view).
Thus, you are free to change the frame of a view, but be aware that if layoutSubviews is sent and the constraints disagree with the frame that you set, the view will jump back to where the constraints say to put it.

Animating views constrained with auto layout

Just for fun I started to play with animations after successfully applying them to some basic background color changes. I have some different images on my "welcome/splash" view that I would like to animate. One image should appear from the bottom, another from the top and so on.
I immediately ran into trouble since I am using auto layout it was not that easy as animating background colors. I found this post How do I animate constraint changes? and after doing what was described at least my image was animating. However, is it correct that the console window should be filled with warnings/info/errors about constraint violations? Also, animating the vertical position of one image causes all the other image to animate too, probably because of some constraint relations.
How are you supposed to deal with that? when animating the "Bottom layout attribute" with a new constant value I expect only the view it belongs to animate, not the whole screen.
And how are you supposed to refer to the constraints? By outlets?
I am creating my views and constraints in storyboard. I deleted my code for animating the image views since it was just a mess. But whats done with the button and textfields in this tutorial is pretty much what I am trying to do with my images. Without animating one image causing the whole view to animate with it. http://www.raywenderlich.com/113674/ios-animation-tutorial-getting-started
However, is it correct that the console window should be filled with warnings/info/errors about constraint violations?
Learning Auto layout can be a challenge which will fill your days with such warnings/info/errors about constraints. Many iOS developer's struggle with Auto Layout at first. Auto layout is an addition to the layout process. The issue here is how are you going to deal with them. I suggest reading the Apple Auto Layout Guide, it contains a section on debugging. Also look at the Debugging Tricks and Tips section.
Here's a great article explaining more of the concepts behind Auto layout.
Build a simply app that has only one view and a subview, so that you can reduce the noise around layout constraint errors in more complicated layouts.
Here is a code snippet of how to animate a constraint.
if myViewTrailingConstraint.constant == -2 {
myViewTrailingConstraint.constant = 200
} else {
myViewTrailingConstraint.constant = -2
}
UIView.animateWithDuration(0.3,
animations: {
self.view.layoutIfNeeded()
},
completion: nil)
How are you supposed to deal with that?
Auto layout is an inter connected system of relationships between your views. A constraint represents a relationship. So you really need to think through your view's layout and plan your constraints first. Why? because if you plan to animate certain views you need to need to make sure that the constraint constant you are going to change will effect only that view in question.
And how are you supposed to refer to the constraints? By outlets?
You can create constraints solely in code or with the interface builder (outlets). I would suggest that you start with interface builder as even when you are comfortable working in code, it is useful and time saving to be able to do your initial layout in interface builder - so learn to use both.
Warnings are not normal - you have to solve them to avoid strange effects.
If animating a constraint value of one image moves other images, then indeed you must have some constraints in place that also change - e.g "equal width" constraints or such. Normally it just works - if it doesn't, you have to show the warnings you get and the constraints you set in order for someone to see what is going wrong.

ScrollView With AutoLayout

I have a weird output with autoLayout used for following scenario.
I have 4 sub pages to scroll. (subPage or scrollPage has been designed separately with autoLayout).
MainView has a scrollView component which loads the sub pages.
Everything is fine except the starting. After first load, the sub page components are not arranged properly. As soon as it receives a first tap/touch, automatically re-scrolls/re-arranges to proper places. which looks like a bug.
The loading creates problem. I have attached 2 images for reference for the above scenarios.
FirstOne at the first loading
Second one just after tapping on scroll area
Second one is the proper one. I need to show this instead of the first.
Need help to fix this.
Thanks,
Satyaranjan
One more thing to note,
[myScroll scrollRectToVisible:CGRectMake(320*pageNumber, 0, 320 , 240) animated:NO];
this is not working As I am using dynamic width for the subPage. Because it will vary for iPhon5 and iPhone6
First, you generally don't want to set the scrollView's contentSize when using autolayout -- if your sub views are laid out correctly, it will do that automatically.
Try this:
constrain all four of scrollView's edges to its parent
make sure translatesAutoresizingMaskIntoConstraints is NO for all views
Create a view called "contentView" and parent it to the scrollView.
pin all four of contentViews's edges to scrollView
parent your subviews to contentView and arrange them with autolayout
If for some reason you need to change the size of a subview programmaticly, you need to override its intrinsicContentSize method to return the correct size after the change. After the change, you might need to call the view's sizeToFit method from its parent view (not sure about that -- it may happen automatically).
In general, when using autolayout, you should almost never explicitly set the size of anything. If it can't be avoided, you should do it by creating height/width constraints and modifying them at runtime in the updateConstraints method.
EDIT:
I made an example project which demonstrates how to set up a scrollView and a couple other things.
Take a look!
https://github.com/annabd351/AutolayoutTemplate

collectionViewContentSize() vs contentSize

What is the difference between
collectionViewController.collectionViewLayout.collectionViewContentSize() and collectionViewController.collectionView.contentSize ?
What do you prefer to use?
contentSize is a property of UIScrollView, whereas collectionViewContentSize() is a method of UICollectionViewLayout.
Reading Programming iOS 7, 4th Edition, whilst summarising UICollectionViewLayout, the author states:
The layout workhorse class for a collection view. A collection view
cannot exist without a layout instance! As I’ve already said, the
layout knows how much room all the subviews occupy, and supplies the
collectionViewContentSize that sets the contentSize of the collection
view, qua scroll view.
From personal experience I encountered difficulties using layout.collectionViewContentSize(). Either the console showed the warning below, or the layouts appeared initially incorrect.
the behavior of the UICollectionViewFlowLayout is not defined because: the item height must be less than the height of the UICollectionView minus the section insets top and bottom values.
My guess is that collectionViewContentSize() does more than just return the content size. I think that invoking this actually initiates the layout calculations. If - during those calculations - anomalies are detected, then the warning shown is output.
For me testing on iPad and initiating the collection view via a Xib, the Xib's frames were iPhone sized. Initial passes triggered from viewDidLayoutSubviews were dealing with the small view. Checking the collection view's frame, it was too small. Subsequent layout engine passes eventually delivered the correct sizes, but by then the warnings had already been displayed.
Next, I tried making the Xib frame much larger. This obviated the error, but caused a worse problem; layouts were initially wrong. In scrolling, you would see the items jump around as the layout was recalculated to the correct dimensions.
Possibly the answer is to call invalidateLayout(), but I'm not sure where. I tried after instantiating the collection view and that didn't work.
So, the answer? I used contentSize. Initial passes in viewDidLayoutSubviews still show incorrect sizes but eventually come good. It doesn't generate any console warnings and better still, doesn't trigger any erroneous layout based upon the old frame size. Even better, rotation is handled automatically as viewDidLayoutSubviews will be called automatically by which point the content size will have been updated.
collectionViewContentSize() is a method you can override (in the layout) to generate the size dynamically.
contentSize is a property of collectionView (or any UIScrollView, for that matter) that is going to be used if there is no such override.
It is similar to a UITableView's rowHeight vs. the UITableViewDelegate's heightForRowAtIndexPath().
Please also note (as mentioned in the comments) that "contentSize is a UIScrollView-inherited property, while itemSize is the property on UICollectionViewLayout that allows you to specify a blanket size for each cell".

iOS AutoLayout - when does it Run?

I have a XIB file where I specify constraints at design time.
In addition to controls with constraints, I also have controls that have no constraints in the XIB that I want to position programmatically at run time.
I do this by repositioning views manually in didRotateFromInterfaceOrientation.
When I rotate the device, my code places the controls that are manually controlled but then later it appears that auto layout messes up the placement of manual controls that have no constraints on them in the Interface builder.
Question - When exactly does Auto layout run if I rotate the device.
Auto Layout runs at the end of the run loop after setNeedsLayout is called on a view, or immediately when setNeedsLayout is followed by layoutIfNeeded. Note that there are many methods that may call setNeedsLayout in their implementation, such as addSubview:, removeFromSuperview, etc.
A couple suggestions:
Instead of updating your constraints in didRotateFromInterfaceOrientation:, try doing so in updateViewConstraints.
You may also want to add placeholder constraints in Interface Builder to the views you will position programmatically. I think IB will automatically constrain views to their x/y position if a placeholder constraint is not set, but I'm not 100% sure on that.
The important thing to know is that the results of constraint calculations are applied in layoutSubviews. So if you want to set frames, you do so after calling super in layoutSubviews or in your view controller's viewDidLayoutSubviews.
Having said that, I've only experimented with this. I've not done it in production code and can't say that you won't run into issues.
I'm actually not clear on whether it's OK to set frames manually like this. The only relevant bit of information I've come across is the following from Apple's Auto Layout documentation:
You cannot set a constraint to cross the view hierarchy if the
hierarchy includes a view that sets the frames of subviews manually in
a custom implementation for the layoutSubviews method on UIView (or
the layout method on NSView).
Here, the phrase "view that sets the frames of subviews manually" implies to me that manually setting frames is OK. I'd be interested to know if anyone has a see a more explicit discussion on this.
you must not change the frame you must connect the constraint with iboulet and in the didRotateFromInterfaceOrientation you can change the constraint like this:
(void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation{
if (UIInterfaceOrientationIsLandscape(fromInterfaceOrientation)) {
self.constraintTop.constant = 50;
}else{
self.constraintTop.constant = 100;
}
}

Resources