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.
Related
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?
In an iPhone word game I have an UIScrollView (holding UIImageView) and 7 draggable custom UIViews placed initially at the bottom (and outside the scroll view):
In the single ViewController.m I have overwritten viewDidLayoutSubviews so that it always sets the zoomScale of the scroll view - to have the UIImageView fill exactly 100% of the screen width.
This works well - for portrait and landscape modes. And when the app is just started:
My problem is however, when I first pinch/zoom/double-tap the scroll view and then move one of the seven Tile.m views:
Suddenly (not every time) viewDidLayoutSubviews is called after touchesBegan.
This resets the zoom of the scroll view - unexpectedly for the user.
My question is if there is any way to disable this behavior?
Is it possible to prevent viewDidLayoutSubviews call of the parent, when its child UIView is being touched/dragged?
UPDATE:
I've moved the zoomScale setting code from viewDidLayoutSubviews to didRotateFromInterfaceOrientation and the scroll view zoom is okay now, but its contentOffset is reset to {0,0}, when (not always) I drag a Tile - i.e. the scroll view jumps suddenly, the app is unusable.
Adding the following to viewDidLoad hasn't helped:
if ([self respondsToSelector:#selector(setAutomaticallyAdjustsScrollViewInsets:)])
self.automaticallyAdjustsScrollViewInsets = NO;
viewDidLayoutSubviews is called whenever the system performs layout on the view (layoutSubviews). This can be called for a plethora of reasons; you can subclass your view, implement an empty layoutSubviews method (don't forget to call the super implementation!) and put a breakpoint there to see who causes the layout. You may also want to implement setNeedsLayout and layoutIfNeeded, and put breakpoints there also for your investigation, as these trigger layout on followup runloops.
But you will never be able to prevent layout. The system performs layout on many occasions which are outside of your control. For example, if a user makes a call, exists the phone app and returns to your app; a layout is triggered because the bounds and frame of the window have changed. The call ends; layout is again triggered because now the window is back to previous size.
You should be responsible for figuring out when to set the zoom scale. For example, if the user starts a gesture, you should signal your code not to perform changes, even if a layout was performed.
This is in continuation to the problem I had here(which is still unresolved): link
But this may help understand what is the problem.
I created just a simple test project ('Empty Application') and added a view controller with a XIB file (check box: 'With XIB file for user interface' selected). Code looks like this:
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog(#"didLoad: %#",NSStringFromCGRect(self.view.bounds));
// Do any additional setup after loading the view from its nib.
}
-(void) viewDidAppear:(BOOL)animated
{
NSLog(#"didAppear: %#",NSStringFromCGRect(self.view.bounds));
}
This is the output:
2013-07-26 17:05:28.502 testtest[5926:c07] didLoad: {{0, 0}, {320, 548}}
2013-07-26 17:05:28.506 testtest[5926:c07] didAppear: {{0, 0}, {320, 460}}
How come they are different?
(ps. I am testing on 6.1 simulator)
When the viewDidLoad method is called, your view controller has only just been loaded from your storyboard or XIB, and so the view dimensions are equal to those that you have in the XIB (those looks like iPhone 5 height dimensions).
Later, when viewDidAppear: is called, the view has already appeared on the screen, so it has been resized appropriately to actually fit on the screen, so its dimensions may be different to those in your storyboard, and consequently different to those that are set when the view is loaded.
In your case, it looks like your storyboard or XIB file is set to iPhone 5 screen size (548 = 1136/2 - status bar), and you are testing in a pre-iPhone 5 simulator or device with a 480x320 point screen, so the view gets resized down to 460 points high to fit on the screen.
This could have perfect sense.
ViewDidLoad is called lazily when first access of controller.view, so by that time the frame is not set yet. This means that you can not rely on the frame/bounds sizes at this point because it will only contain a default value (although in many cases it will be correct).
In ViewDidAppear, the frame is usually set, although if your parent controller is setting any animation you could also have a temporal frame state instead of the final one, but it is not usual as by convention this method is called when the view is already displayed.
For example, if you are loading the view from an IB file, the frame you will get in the viewDidLoad is the one you have in the IB file, but maybe the final size for your view is smaller/bigger, and then you will get another one in your viewDidAppear.
Instead of that, you should create all your elements resizable (use Spring&Struts, AutoLayout or any other similar alternative) so they will be properly displayed when the frame is set.
When a ViewController presents its view, it normally shrinks that
view so that its frame does not overlap the device’s status bar.
So when you NSLog in viewDidLoad, the View i not yet loaded so ViewController has not shrinked the frame yet but in viewDidAppear , it has done the resizing.
There is a property in UIViewController
wantsFullScreenLayout
Setting this property to YES causes the view controller to size its
view so that it fills the entire screen, including the area under the
status bar. (Of course, for this to happen, the window hosting the
view controller must itself be sized to fill the entire screen,
including the area underneath the status bar.) You would typically set
this property to YES in cases where you have a translucent status bar
and want your view’s content to be visible behind that view.
As far as i know, ViewDidLoad will set the bounds for your application as defined, in the RootViewController/XIB-file for root view, or maybe the AppDelegate.
If you define the applications bounds there (not sure in which one of these), ViewDidLoad functions in the entire app, will initially, set the height according to that.
Once the View is loaded, and ready to 'appear', actual bounds may be requested.
Hence it is advised to request bounds/sizes in the ViewWillAppear/ViewDidAppear methods.
-viewDidLoad is called the first time viewController.view is called. It is called before the view is returned. This has a very important implication. In order for the view to be shown or sized, it needs to be added to the window or some other view.
How does this happen? It looks something like [window addSubview:viewController.view].
So, the fully loaded view is needed before it can be placed into the view hierarchy. This places limitations on self.view when used within -viewDidLoad.
self.view.superview and self.view.window will always be nil.
self.view is not yet sized to fit into self.view.superview.
With this understanding, you can see how self.view cannot know it's final size when -viewDidLoad is called.
UPDATE
If you need to apply a custom layout to a view's subviews, you may want to consider subclassing UIView and using -[UIView layoutSubviews].
Barring that, are you having trouble with -viewWillAppear: instead of -viewDidAppear:? -viewWillAppear: is called before the view is displayed, so you won't have odd visual effects when you resize the view.
I have a view controller with a UILabel in it that prints some words when a button is tapped. When the button is tapped, the navigation bar is set to hidden.
So I tried taking the UILabel and giving it these constraints in Interface Builder:
But with those, when I press the button, the UILabel jumps down with the nav bar disappearing, and then back up again, correcting itself, looking terrible. It should stay in its place permanently, no matter what goes on with the nav bar.
Here's a direct link to a short video showing what happens.
How would I best go about setting it so the UILabel stays in place?
Project: http://cl.ly/1T2K0V3w1P21
When you tell the navigation controller to hide the navigation bar, it resizes its content view (your ReadingViewController's view) to be full-screen, and the content view lays out its subviews for the new full-screen size. By default, it does this layout outside of any animation block, so the new layout takes effect instantly.
To fix it, you need to make the view perform layout inside an animation block. Fortunately, the SDK includes a constant for duration of the animation that hides the navigation bar, and the animation uses a linear curve. Change your hideControls: method to this:
- (void)hideControls:(BOOL)visible {
[UIView animateWithDuration:UINavigationControllerHideShowBarDuration animations:^{
[self.navigationController setNavigationBarHidden:visible animated:YES];
self.backFiftyWordsButton.hidden = visible;
self.forwardFiftyWordsButton.hidden = visible;
self.WPMLabel.hidden = visible;
self.timeRemainingLabel.hidden = visible;
[self.view layoutIfNeeded];
}];
}
There are two changes here. One is that I've wrapped the method body in an animation block using the UINavigationControllerHideShowBarDuration constant, so the animation has the correct duration. The other change is that I send layoutIfNeeded to the view, inside the animation block, so the views will animate to their new frames.
Here's the result:
You could also use this animation block to fade your labels in and out by changing their alpha properties instead of their hidden properties.
UPDATE
In response to the questions in your comment:
First, you need to understand the phases of the run loop. Your app is always running a loop on its main thread. The loop, extremely simplified, looks like this:
while (1) {
wait for an event (touch, timer, local or push notification, etc.)
Event phase: dispatch the event as appropriate (this often ends up
calling into your code, for example calling your tap recognizer's action)
Layout phase: send `layoutSubviews` to every view in the on-screen
view hierarchy that has been marked as needing layout
Draw phase: send `drawRect:` to any view that has been marked as needing
display (because it's a new view or it received `setNeedsDisplay` or
it has `UIViewContentModeRedraw`)
}
For example, if you put a breakpoint in hideControls:, tap the screen, and then look at the stack trace in the debugger, you'll see PurpleEventCallback way down in the trace (right above __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__). This tells you you're in the event handling phase. (Purple was the code name of the iPhone project inside Apple.)
If you see CA::Transaction::observer_callback, you're in either the layout phase or the draw phase. Further up the stack you'll see either CA::Layer::layout_if_needed or CA::Layer::display_if_needed depending on which phase you're in.
So that's the run loop and its phases. Now, when does a view get marked as needing layout? It gets marked as needing layout when it receives setNeedsLayout. You can send this if, for example, you've changed the content your views should display and they need to be moved or resized accordingly. But the view will send itself setNeedsLayout automatically in two cases: when the size of its bounds changes (or the size of its frame), and when its subviews array changes.
Note that changing the view's size or its subviews does not make the view lay out its subviews immediately! It's simply scheduled to lay out its subviews later, during the layout phase of the run loop.
So... what does this all have to do with you?
In your hideControls: method, you do [self.navigationController setNavigationBarHidden:visible animated:YES]. Suppose visible is NO. Here's what the navigation controller does in response:
It begins an animation block.
It sets the position of the navigation bar to above the top of the screen.
It increases the height of the content view by 44 points (the height of the navigation bar).
It decreases the Y coordinate of the content view by 44 points.
It ends the animation block.
The changes to the content view's frame cause the content view to send itself setNeedsLayout.
Note that the changes to the navigation bar's frame and the content view's frame are animated. But the frames of the content view's subviews have not change yet. Those changes happen later, during the layout phase.
So the navigation controller animates the changes to your top-level content view, but it doesn't animate changes to the subviews of your content view. You have to force those changes to be animated.
You force those changes to be animated by taking two steps:
You create an animation block whose parameters match the parameters used by the navigation controller.
Inside that animation block, you force the layout phase to happen immediately, by sending layoutIfNeeded to the content view.
The layoutIfNeeded documentation says this:
Use this method to force the layout of subviews before drawing. Starting with the receiver, this method traverses upward through the view hierarchy as long as superviews require layout. Then it lays out the entire tree beneath that ancestor.
It lays out the entire tree by sending layoutSubviews messages to the views in the tree, in order from root to leaves. If you're not using auto layout, it also applies the autoresizing mask of each view's subviews before sending layoutSubviews to the view.
So by sending layoutIfNeeded to your content view, you are forcing auto layout to update the frames of your content view's subviews immediately, before layoutIfNeeded returns. This means those changes happen inside your animation block, so they are animated with the same parameters (duration and curve) as the changes to the navigation bar and your content view.
Laying out subviews in an animation block is so important that Apple defined an animation option, UIViewAnimationOptionLayoutSubviews. If you specify this option, then at the end of the animation block, it will automatically send layoutIfNeeded. But using that option requires using the long version of the message, animateWithDuration:delay:options:animations:completion:, so it's usually easier to just do [self.view layoutIfNeeded] yourself at the end of the block.
Set a constraint form the botton, lead and trail and one to a fixed height.
(Copying my answer from the question you posted that is marked as duplicate: I have a UILabel positioned on the screen with autolayout, but when I hide the navigation bar it causes the label to "twitch" for a second)
Instead of bottom space constraint, you can try to define the top space constraint to the superview from the label (which is 22 in the constant), connect it as an IBOutlet to your view property, and animate it when the navigation bar is hidden or shown.
For example, I declare the top space property as topSpaceConstraint:
#property (weak, nonatomic) IBOutlet NSLayoutConstraint *topSpaceConstraint;
Then inside the hideControls method, I can animate the constraint:
- (void)hideControls:(BOOL)visible {
if (visible) {
[UIView animateWithDuration:UINavigationControllerHideShowBarDuration animations:^{
self.topSpaceConstraint.constant = 66; //44 is the navigation bar height, you need to find a way not to hardcode this
[self.view layoutIfNeeded];
}];
}
else {
[UIView animateWithDuration:UINavigationControllerHideShowBarDuration animations:^{
self.topSpaceConstraint.constant = 22;
[self.view layoutIfNeeded];
}];
}
[self.navigationController setNavigationBarHidden:visible animated:YES];
self.backFiftyWordsButton.hidden = visible;
self.forwardFiftyWordsButton.hidden = visible;
self.WPMLabel.hidden = visible;
self.timeRemainingLabel.hidden = visible;
}
Have a very large program where there is always a superview that just encompasses a custom segment controller. This view sits at the top of screen and controls navigation in several ways.
So the problem arose in only a selected few view controllers where everything was 100% programmaticly created. Essentially CGRect are not being defined in the property dynamic coordinates. But are not being recalculated on orientation change. Does anyone have a simple way to control this in the subview? I'm about to code something in the superview to pass to orientation to other subviews.. but there has to be a better way. Ideas?
Couple of pointers:
You can use auto-resizing masks to determine what happens to your views when their bounds change (ie, when the orientation changes). So UIViewAutoresizingFlexibleWidth means your view will 'stretch' proportionally with the superview when the bounds are changed. UIViewAutoresizingFlexibleLeftMargin means your view will effectively be right-aligned, as the left margin will adjust according to the width, etc etc.
Sometimes auto-resizing masks aren't enough - perhaps you have to change the view's content on an orientation, or do a complex animation. In this case, you use the willAnimateRotationToInterfaceOrientation method in your view controller. Your subviews might have a custom adjustForOrientation method that you've written that you can trigger when willAnimateRotation... is called.
Finally, on iOS 5 you can actually nest view controllers inside of view controllers, in which case orientation events get passed through automatically...but this is probably needlessly complex for what you're trying to do.