Auto Layout = ViewWillAppear has the wrong width of the view - ios

I am using autolayout for parts of my UI. When I check the width of my view in viewWillAppear, the width has not yet updated. It gets updated after the second callback to - (void)updateViewConstraints. The issue is I set up my parts of my UI (using spring and struts for legacy reasons) based on this width in viewWillAppear. When is the good time to set up this legacy UI which takes in a width parameter ?
thanks

Layout happens after viewWillAppear returns. Do it in viewDidLayoutSubviews (in your view controller) or in layoutSubviews (in your custom view subclass). Either way, be sure to call super first.
Another way is to force layout by sending [view layoutIfNeeded]. Then you can add subviews and set their springs and struts. The system will perform layout again (at its usual time) because you added subviews.

Related

Why viewDidLayoutSubviews is called twice only on initialize?

I am going to use it by adding a child VC. (Coding with code)
ChildVC is using autolayout (snapkit).
At initialization, viewDidLayoutSubviews is called twice.
At the first call, the childVC.View is set to full screen Frame.
In the second call, it is set according to the autolayout setting.
I found out that if the child VC's Frame is not set, it is set to full screen.
(Resize View From NIB)
I want to know how to turn off Resize View From NIB programmatically.
The problem is, I want to know why viewDidLayoutSubviews is called twice.
I'm curious as to why the childVC.View size changes when the second call is made.
I want to know one more thing. Isn't this usually used when setting subView's witdh and Height?
someView.snp.makeConstraints { make in
make.top.left.right.equalToSuperview()
make.width.equalTo(view.bounds.width)
make.height.equalTo(view.bounds.height * 0.58)
}
Based on VC.View bounds ratio?

Can layoutIfNeeded be used in viewDidLoad to determine a corner radius size?

Is it safe to call layoutIfNeeded inside viewDidLoad in an Xcode 8 iOS project like the code below?
- (void)viewDidLoad {
[super viewDidLoad];
[self.view layoutIfNeeded];
CGFloat frameHeight = CGRectGetHeight(self.frame);
subview.layer.cornerRadius = frameHeight * 0.5;
}
It works fine in Xcode 7 without layoutIfNeeded.
You can call it in viewDidLayoutSubviews() if you have issue.
From Apple iOS 10 Release notes is:
Sending layoutIfNeeded to a view is not expected to move the view, but in earlier releases, if the view had translatesAutoresizingMaskIntoConstraints set to NO, and if it was being positioned by constraints, layoutIfNeeded would move the view to match the layout engine before sending layout to the subtree. These changes correct this behavior, and the receiver’s position and usually its size won’t be affected by layoutIfNeeded.
Some existing code may be relying on this incorrect behavior that is now corrected. There is no behavior change for binaries linked before iOS 10, but when building on iOS 10 you may need to correct some situations by sending -layoutIfNeeded to a superview of the translatesAutoresizingMaskIntoConstraints view that was the previous receiver, or else positioning and sizing it before (or after, depending on your desired behavior) layoutIfNeeded.
Third party apps with custom UIView subclasses using Auto Layout that override layoutSubviews and dirty layout on self before calling super are at risk of triggering a layout feedback loop when they rebuild on iOS 10. When they are correctly sent subsequent layoutSubviews calls they must be sure to stop dirtying layout on self at some point (note that this call was skipped in release prior to iOS 10)."
Essentially you cannot call layoutIfNeeded on a child object of the View - now calling layoutIfNeeded has to be on the superView, and you can still call this in viewDidLayoutSubviews.
I think a better approach would be to do this either in viewWillAppear or viewDidLayoutSubviews
Those are times when the frame is actually finalized. viewDidLayoutSubviews is good because it will react to changes in the views frame.
If you have a requirement of different cornerRadius values for different size classes, I would use traitCollectionDidChange instead of viewWillLayoutSubviews or viewDidLayoutSubviews.
override func traitCollectionDidChange(previousTraitCollection: UITraitCollection?) is called only when the iOS interface environment changes. If the view is being loaded for the first time, previousTraitCollection will be nil.
That way, if your application supports multitasking, as soon as the view size changes to show two apps on your device screen at once, your views and will resize properly to fit the new size class. Using the layoutSubviews methods will also work, but might put additional processing strain on your application.

What conditions can prevent layoutSubviews from being called after setNeedsLayout?

The problem
In a custom view of mine (a UIScrollView subclass) I am calling setNeedsLayout in response to a "reload data" event (triggered by an external source). Most of the time this works correctly and layoutSubviews is called when the next view layout cycle occurs. Sometimes, however, layoutSubviews is not called! Up until now I was living with the "certain knowledge" that setNeedsLayout always triggers layoutSubviews. Apparently I was wrong. I even tried calling layoutIfNeeded after setNeedsLayout, but still no success.
The question
Obviously, I would like to solve my particular problem. On the other hand, I would like to improve my understanding of the view layout process on iOS, so I am formulating my question in a general way: Do you know of any conditions that can prevent layoutSubviews from being called after setNeedsLayout has been called? Answers that focus on UIScrollView are quite welcome, since that is where I am having trouble.
Problem context
I am on iOS 7.1, using Xcode 5.1.1. Notes on the implementation of my custom scroll view:
The scroll view has a single container view of type UIView that is always the same size as the scroll view content size
The scroll view's layoutSubviews implementation places custom subviews into the container view by manually calculating the subviews' frames
The custom subviews' implementation uses Auto Layout
Here is how the reloadData implementation looks like:
- (void) reloadData
{
// Iterates through an internal array that holds the subviews,
// then empties the array. Subviews are deallocated at this
// point.
[self removeAllSubviewsFromContainerview];
self.contentOffset = CGPointMake(0, 0);
// I verified that the content size is always greater than
// CGSizeZero (both width and height)
CGFloat contentWidth = [self calculateNewContentWidth];
self.contentSize = CGSizeMake(contentWidth, self.frame.size.height);
// Here is the problem: Sometimes this triggers layoutSubviews,
// sometimes it does not.
[self setNeedsLayout];
// Adding the following line for debugging purposes does not
// help, making it clear that setNeedsLayout has no effect.
// [self layoutIfNeeded];
}
Last but not least, here are some observations I made:
layoutSubviews is called as expected if the content offset or content size change. layoutSubviews is not called in my particular case if these two values don't change. I first assumed that this observation is a general working principle of UIScrollView, i.e. that layoutSubviews is generally not called for UIScrollView unless content offset or content size change. However, when I wrote a couple of minimal projects I failed to reproduce this assumed "general" behaviour - in those projects layoutSubviews was always called as expected, regardless of whether content offset or content size changed or not.
layoutSubviews is always called as expected if the scroll view displays dummy UILabel instances instead of my own custom subviews
layoutSubviews is always called as expected if I use my own custom subviews, but replace Auto Layout in the custom subviews' implementation by manual frame calculations
The last two observations have led me to believe that Auto Layout is somehow involved, but I really have no idea how this could affect how setNeedsLayout works.

iOS: How can I figure out the size of the view in the super view's layout?

When I use self.view.frame.size.height, it gets the height of the child nib, rather than the height of how the view is going to fit into the overall layout. I get the same value using self.view.bounds.size.height and super.view.bounds.size.height.
Any help would be appreciated.
Ask at the right time. Not until the view is ready to appear has iOS figured out what size it will actually be. Move your code that depends upon the view being the right size to viewWillAppear instead of viewDidLoad or an init method.
In Xcode 4 you would select your view controller that contains the UIScrollView and set the 'Simulated Metrics' in the IB Attributes Inspector as follows:
This ensures that your UIScrollView will be set to the correct size to fill the available space at design time. It looks like you are using an older version of Xcode/IB and I can't remember exactly where these settings used to be but they're in there somewhere.
With the layout designed to accommodate the space taken up by the tab bar you should be able to query the size of the UIScrollView at any time and get the correct values.

When is layoutSubviews called?

I have a custom view that's not getting layoutSubview messages during animation.
I have a view that fills the screen. It has a custom subview at the bottom of the screen that correctly resizes in Interface Builder if I change the height of the nav bar. layoutSubviews is called when the view is created, but never again. My subviews are correctly laid out. If I toggle the in-call status bar off, the subview's layoutSubviews is not called at all, even though the main view does animate its resize.
Under what circumstances is layoutSubviews actually called?
I have autoresizesSubviews set to NO for my custom view. And in Interface Builder I have the top and bottom struts and the vertical arrow set.
Another part of the puzzle is that the window must be made key:
[window makeKeyAndVisible];
of else the subviews are not automatically resized.
I had a similar question, but wasn't satisfied with the answer (or any I could find on the net), so I tried it in practice and here is what I got:
init does not cause layoutSubviews to
be called (duh)
addSubview: causes
layoutSubviews to be called on the
view being added, the view it’s being
added to (target view), and all the
subviews of the target
view setFrame
intelligently calls layoutSubviews on
the view having its frame set only
if the size parameter of the frame is
different
scrolling a UIScrollView
causes layoutSubviews to be called on
the scrollView, and its superview
rotating a device only calls
layoutSubview on the parent view (the
responding viewControllers primary
view)
Resizing a view will call layoutSubviews on its superview (Important: views with an intrinsic content size will re-size if the content that determines their size changes; for example, updating the text on a UILabel will cause the intrinsic content size to be updated and thus call layoutSubviews on its superview)
My results - http://blog.logichigh.com/2011/03/16/when-does-layoutsubviews-get-called/
Building on the previous answer by #BadPirate, I experimented a bit further and came up with some clarifications/corrections. I found that layoutSubviews: will be called on a view if and only if:
Its own bounds (not frame) changed.
The bounds of one of its direct subviews changed.
A subview is added to the view or removed from the view.
Some relevant details:
The bounds are considered changed only if the new value is different, including a different origin. Note specifically that is why layoutSubviews: is called whenever a UIScrollView scrolls, as it performs the scrolling by changing its bounds' origin.
Changing the frame will only change the bounds if the size has changed, as this is the only thing propagated to the bounds property.
A change in bounds of a view that is not yet in a view hierarchy will result in a call to layoutSubviews: when the view is eventually added to a view hierarchy.
And just for completeness: these triggers do not directly call layoutSubviews, but rather call setNeedsLayout, which sets/raises a flag. Each iteration of the run loop, for all views in the view hierarchy, this flag is checked. For each view where the flag is found raised, layoutSubviews: is called on it and the flag is reset. Views higher up the hierarchy will be checked/called first.
https://developer.apple.com/library/prerelease/tvos/documentation/WindowsViews/Conceptual/ViewPG_iPhoneOS/CreatingViews/CreatingViews.html#//apple_ref/doc/uid/TP40009503-CH5-SW1
Layout changes can occur whenever any of the following events happens
in a view:
a. The size of a view’s bounds rectangle changes.
b. An interface orientation change occurs, which usually triggers a change in the root view’s bounds rectangle.
c. The set of Core Animation sublayers associated with the view’s layer changes and requires layout.
d. Your application forces layout to occur by calling the setNeedsLayout or layoutIfNeeded method of a view.
e. Your application forces layout by calling the setNeedsLayout method of the view’s underlying layer object.
Some of the points in BadPirate's answer are only partially true:
For addSubView point
addSubview causes layoutSubviews to be called on the view being added, the view it’s being added to (target view), and all the subviews of the target.
It depends on the view's (target view) autoresize mask. If it has autoresize mask ON, layoutSubview will be called on each addSubview. If it has no autoresize mask then layoutSubview will be called only when the view's (target View) frame size changes.
Example: if you created UIView programmatically (it has no autoresize mask by default), LayoutSubview will be called only when UIView frame changes not on every addSubview.
It is through this technique that the performance of the application also increases.
For the device rotation point
Rotating a device only calls layoutSubview on the parent view (the responding viewController's primary view)
This can be true only when your VC is in the VC hierarchy (root at window.rootViewController), well this is most common case. In iOS 5, if you create a VC, but it is not added into any another VC, then this VC would not get any noticed when device rotate. Therefore its view would not get noticed by calling layoutSubviews.
I tracked the solution down to Interface Builder's insistence that springs cannot be changed on a view that has the simulated screen elements turned on (status bar, etc.). Since the springs were off for the main view, that view could not change size and hence was scrolled down in its entirety when the in-call bar appeared.
Turning the simulated features off, then resizing the view and setting the springs correctly caused the animation to occur and my method to be called.
An extra problem in debugging this is that the simulator quits the app when the in-call status is toggled via the menu. Quit app = no debugger.
calling
[self.view setNeedsLayout];
in viewController makes it to call viewDidLayoutSubviews
have you looked at layoutIfNeeded?
The documentation snippet is below. Does the animation work if you call this method explicitly during the animation?
layoutIfNeeded
Lays out the subviews if needed.
- (void)layoutIfNeeded
Discussion
Use this method to force the layout of subviews before drawing.
Availability
Available in iPhone OS 2.0 and later.
When migrating an OpenGL app from SDK 3 to 4, layoutSubviews was not called anymore. After a lot of trial and error I finally opened MainWindow.xib, selected the Window object, in the inspector chose Window Attributes tab (leftmost) and checked "Visible at launch". It seems that in SDK 3 it still used to cause a layoutSubViews call, but not in 4.
6 hours of frustration put to an end.

Resources