Is there an iOS method that fires when Autolayout has completed? - ios

I have an iOS app in which I need to know when a new view is completely visible on-screen; that is, when Autolayout has finished its calculations and the view has finished drawing.
ViewDidAppear seems to fire well before the view is completely visible. If I turn off Autolayout, the timing seems to line up as far as human perception goes, but I need to use Autolayout in this project (so this isn't a solution...just a test).
Is there any method that fires when Autolayout is done calculating? Or another method that fires when the view is ACTUALLY visible (since ViewDidAppear doesn't work for this)?
Thanks!

The following can be used to avoid multiple calls:
- (void) didFinishAutoLayout {
// Do some stuff here.
NSLog(#"didFinishAutoLayout");
}
and
- (void) viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
[NSObject cancelPreviousPerformRequestsWithTarget:self
selector:#selector(didFinishAutoLayout)
object:nil];
[self performSelector:#selector(didFinishAutoLayout) withObject:nil
afterDelay:0];
}

I'm using viewDidLayoutSubviews for this. Apple's documentation says, "Called to notify the view controller that its view has just laid out its subviews."

If you watched 2018's WWDC about "High-Performance AutoLayout", you would know the answer to this question.
Technically, there is no such API method that will be called when autolayout has completed your view's layout. But when autolayout has completed the calculations, your view's setBounds and setCenter will be called so that your view gets its size and position.
After this, your view's layoutSubviews will be called. So, layoutSubviews can, to some degree, be thought of as the method that fires after autolayout has done calculations.
As to view controller's viewDidLayoutSubviews, this is a bit complicated. The documentation says:
When the bounds change for a view controller's view, the view adjusts the positions of its subviews and then the system calls this method. However, this method being called does not indicate that the individual layouts of the view's subviews have been adjusted. Each subview is responsible for adjusting its own layout.
So when viewDidLayoutSubviews called on a view controller, only the view controller'view 's first-level subviews are guaranteed to be laid out correctly.

What it worked in my case was request layout after changed a constraint value:
self.cnsTableviewHeight.constant = 50;
[self layoutIfNeeded];
Later on override layoutSubviews method:
- (void) layoutSubviews { //This method when auto layout engine finishes
}
You can call setNeedsLayout also instead of layoutIfNeeded

I guess implementing viewDidLayoutSubviews is the correct way but I used an animation just to write the completion callback inside the same method.
someConstraint.constant = 100; // the change
// Animate just to make sure the constraint change is fully applied
[UIView animateWithDuration:0.1f animations:^{
[self.view setNeedsLayout];
} completion:^(BOOL finished) {
// Here do whatever you need to do after constraint change
}];

You might face this problem not just with UIViewControllers but also UIViews. If you have a subview and want to know if AutoLayout has updated it's bounds, here is the Swift 5 implementation,
var viewBounds: CGFloat = 0.0
var autoLayoutHasCompleted: Bool = false
override func awakeFromNib() {
super.awakeFromNib()
// someSubView is the name of a view you want to check has changed
viewBounds = someSubView.bounds.width
}
override func layoutSubviews() {
if viewBounds != someSubView.bounds.width && !autoLayoutHasCompleted {
// Place your code here
autoLayoutHasCompleted = true
}
}

Related

When the autolayout constraints set frames during view controller life cycle?

I have used autolayout constraints from storyboard. However in some cases, I want to calculate dynamic height of subview. I code this in viewDidAppear(), it works fine because this method is called after all view frames are set by layout constraints. The problem here is that I can see the frame set by constraints for half a second. And then the code reframes the view.
I came to know about viewDidLayout() which is called after constraints has set the frame so I can change. But it doesn't work. It is like this method is called before constraints are used.
The viewDidAppear method is called at the end of the view life cycle. So if you change the constraints here, it will always be visible.
If the change you want to do is a one time event, then you may do so in the method viewWillAppear. But this won't always help because the view drawing may not always be finished by this time. So a better option is to place the code in viewWillLayoutSubviews or viewDidLayoutSubviews.
NOTE: viewWillLayoutSubviews or viewDidLayoutSubviews will be called multiple times in a view's life cycle, such as orientation change or moving in and out of another view or any frame change for that matter. If the code change you want to put here is a one time event, then please make sure to use flags.
eg:-
- (void)viewWillLayoutSubviews {
if(firstTime) {
// put your constraint change code here.
}
}
Hope this helps! :)
As name suggests, viewDidLayoutSubviews is called when the view of your viewController has just finished its laying out and you can assume that at that dynamic height your views are in their correct places/frames according to your autolayout constraints.
Swift :
override func viewDidLayoutSubviews() {
// Set your constraint here
}
Objective C :
-(void)viewDidLayoutSubviews {
// Set your constraint here
}
I am not sure what you are actually trying to do in viewDidLayoutSubviews(). I always use to customise view by modifying layout constraint values overriding this method.
// Called to notify the view controller that its view has just laid out its subviews
override func viewDidLayoutSubviews() {
//Write your code here, any constraint modification etc.
super.viewDidLayoutSubviews()
}

iOS: Can a UIView know that the layout process for it has completed?

When using auto layout, the view's size is unknown when it is initialised, this brings a problem to me.
When using UIImageView, I wrote a category that can load image from my own CDN by setting the image URL to UIImageView, my CDN stores one image with different sizes so that difference devices can load the size it really needs.
I want to make my UIImageView be able to load the URL for the resolution it needs, but when my UIImageView get the URL, the size of it is not yet determined by auto layout.
So is there a way for UIView to know that the layout process for it has finished for the first time?
there is a method for UIView.You could override it.
-(void)layoutSubviews {
CGRect bounds =self.bounds;
//build your imageView's frame here
self.imageView=imageViewFrame.
}
In Swift 5.X
override func layoutSubviews() {
super.layoutSubviews()
let myFrame = = self.bounds
}
If you have other complex items in the custom view, don't forget to call super if you override layoutSubviews()...
Sadly, there is no straightforward way that a UIView can be notified that its constraints have been set. You can try a bunch of different things though,
Implement layoutSubviews function of a UIView, this is called whenever UIView's layout is changed.
Implement viewDidLayoutSubviews of the UIViewController that has it inside it. This function is called when all the layouts have been set. At this point you can your category function.
here's a test, I only use autolayout and I only use custom subclassed subviews. I do all auto layout in the initializer of the subclass:
This is for a "login button" that has no frame but is then set with autolayout:
-(void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
if (self.contentView.loginButton.frame.size.height) {
NSLog(#"viewDidLayoutSubviews");
}
}
-(void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
if (self.contentView.loginButton.frame.size.height) {
NSLog(#"viewWillLayoutSubviews");
}
}
-(void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
}
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
if (self.contentView.loginButton.frame.size.height) {
NSLog(#"didAppear");
}
}
-(void)viewWillAppear:(BOOL)animated {
if (self.contentView.loginButton.frame.size.height) {
NSLog(#"viewWillAppear");
}
}
-(void)viewDidLoad {
[super viewDidLoad];
if (self.contentView.loginButton.frame.size.height) {
NSLog(#"viewDidLoad");
}
}
Here's the output. So, this means that my view finally has a frame when the
2015-08-25 01:28:27.789 [67502:1183631] viewDidLayoutSubviews
2015-08-25 01:28:27.790 [67502:1183631] viewWillLayoutSubviews
2015-08-25 01:28:27.790 [67502:1183631] viewDidLayoutSubviews
2015-08-25 01:28:28.007 [67502:1183631] didAppear
This means that the first time the login button has a frame is in the viewDidLayoutSubviews, this will look weird becuase this is the first pass of main view's subviews. There's no frame in ViewWillAppear although the view of the viewcontroller itself is already set before the login button. The entire UIView subclass's main view is also set when the viewcontroler's view is set, this happens before the login button as well. So, the subviews of the view are set after the parent view is set.
The point is this: if you plop the imageview information pull in the viewDidLayoutSubviews then you have a frame to work with, unless you set this UIImageView frame to the view of the ViewController by type casting then you will have the UIImageView's frame set in the viewDidLoad. Good luck!

Reloading viewDidAppear so it will function as ViewDidLoad

In my viewDidAppear i"m changing the frame of one of my ImageViews. The view and all the other methods will not show it until i will [self viewDidAppear] it.
I feel its not right, is there some reloadData message ?
Thank you.
Exactly, calling [self viewDidappear] yourself is not right.
If you need to change the frame of the views while being on the view, create a method yourself that you can call every time you want, or use viewDidLayoutSubviews;
When the bounds change for a view controller’s view, the view adjusts the positions of its subviews and then the system calls this method. However, this method being called does not indicate that the individual layouts of the view’s subviews have been adjusted. Each subview is responsible for adjusting its own layout.
Also, check that your method has a correct implementation:
-(void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
//Your code
}
It would be better to change your image view's frame in viewDidLayoutSubviews.
You probably implemented the wrong function. The proper method signature is:
- (void)viewDidAppear:(BOOL)animated
not just viewDidAppear.

Setting contentOffset programmatically triggers scrollViewDidScroll

I've got a a few UIScrollView on a page. You can scroll them independently or lock them together and scroll them as one. The problem occurs when they are locked.
I use UIScrollViewDelegate and scrollViewDidScroll: to track movement. I query the contentOffset of the UIScrollView which changed and then reflect change to other scroll views by setting their contentOffset property to match.
Great.... except I noticed a lot of extra calls. Programmatically changing the contentOffset of my scroll views triggers the delegate method scrollViewDidScroll: to be called. I've tried using setContentOffset:animated: instead, but I'm still getting the trigger on the delegate.
How can I modify my contentOffsets programmatically to not trigger scrollViewDidScroll:?
Implementation notes....
Each UIScrollView is part of a custom UIView which uses delegate pattern to call back to the presenting UIViewController subclass that handles coordinating the various contentOffset values.
It is possible to change the content offset of a UIScrollView without triggering the delegate callback scrollViewDidScroll:, by setting the bounds of the UIScrollView with the origin set to the desired content offset.
CGRect scrollBounds = scrollView.bounds;
scrollBounds.origin = desiredContentOffset;
scrollView.bounds = scrollBounds;
Try
id scrollDelegate = scrollView.delegate;
scrollView.delegate = nil;
scrollView.contentOffset = point;
scrollView.delegate = scrollDelegate;
Worked for me.
What about using existing properties of UIScrollView?
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if (scrollView.isTracking || scrollView.isDragging || scrollView.isDecelerating) {
/// The content offset was changed programmatically.
/// Your code goes here.
}
}
Another approach is to add some logic in your scrollViewDidScroll delegate to determine whether or not the change in content offset was triggered programatically or by the user's touch.
Add an 'isManualScroll' boolean variable to your class.
Set its initial value to false.
In scrollViewWillBeginDragging set it to true.
In your scrollViewDidScroll check to see that is it true and only respond if it is.
In scrollViewDidEndDecelerating set it to false.
In scrollViewWillEndDragging add logic to set it to false if the velocity is 0 (as scrollViewDidEndDecelerating won't be called in this case).
Simplifying #Tark's answer, you can position the scrollview without firing scrollViewDidScroll in one line like this:
scrollView.bounds.origin = CGPoint(x:0, y:100); // whatever values you'd like
This is not a direct answer to the question, but if you are getting what appear to be spurious such messages, it can ALSO be because you are changing the bounds. I am using some Apple sample code with a "tilePages" method that removes and adds subview to a scrollview. This infrequently results in additional scrollViewDidScroll: messages called immediately, so you get into a recursion which you for sure didn't expect. In my case I got a nasty impossible to find crash.
What I ended up doing was queuing the call on the main queue:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if(scrollView == yourScrollView) {
// dispatch fixes some recursive call to scrollViewDidScroll in tilePages (related to removeFromSuperView)
// The reason can be found here: http://stackoverflow.com/questions/9418311
dispatch_async(dispatch_get_main_queue(), ^{ [self tilePages]; });
}
}

Is layoutSubviews called whenever view's size is changed?

In my impression, with autoresizesSubviews = YES, layoutSubviews should be called every time view's size is changed. But I found it is not the case for my view. Is my expectation wrong?
According to sources at Apple,
"-[UIView layoutSubviews] should get called when the size of the view changes."
They also referred me to this, from the the View Programming Guide for iOS:
"Whenever the size of a view changes, UIKit applies the autoresizing behaviors of that view’s subviews and then calls the layoutSubviews method of the view to let it make manual changes. You can implement the layoutSubviews method in custom views when the autoresizing behaviors by themselves do not yield the results you want."
At this point, your best move is to create a small sample project where layoutSubviews does not get called (or, send your existing project) file a bug with Apple using BugReporter, and include that sample project with your bug.
If you need something to happen when your view is resized, you can also override setBounds: and setFrame: for your class to make sure it happens. It would look something like this
-(void)setBounds:(GCRect newBounds) {
// let the UIKit do what it would normally do
[super setBounds:newBounds];
// set the flag to tell UIKit that you'd like your layoutSubviews called
[self setNeedsLayout];
}
-(void)setFrame:(CGRect newFrame) {
// let the UIKit do what it would normally do
[super setFrame:newFrame];
// set the flag to tell UIKit that you'd like your layoutSubviews called
[self setNeedsLayout];
}
The other reason that I sometimes override these methods (temporarily) is so I can stop in the debugger and see when they are getting called and by what code.
From my understanding, layoutSubviews is called when the view's bounds change. This means that if its position changes in its superview (but not its size) then layoutSubviews won't be changed (since the origin point in the bounds is in the view's coordinate system - so it is almost always 0,0). In short, only a change in size will cause this to be fired.
whenever you want to resize the views manually and resizes automatically call layoutSubViews method
-(void)layoutSubviews
{
[super layoutSubviews];
CGRect contentRect = self.contentView.bounds;
CGFloat boundsX = contentRect.origin.x;
CGRect frame,itemlabelframe,statuslabelframe;
frame= CGRectMake(boundsX+1 ,0, 97, 50);
itemlabelframe=CGRectMake(boundsX+100, 0, 155, 50);
statuslabelframe=CGRectMake(boundsX+257, 0, 50, 50);
ItemDescButton.frame=itemlabelframe;
priorityButton.frame = frame;
statusButton.frame=statuslabelframe;
// ItemDescLabel.frame=itemlabelframe;
// statusLabel.frame=statuslabelframe;
}

Resources