Is there a callback function when UIView's frame change? - ios

Suppose that I need to do something whenever a view's width or height changes. When not using autoLayout, I can do this by implementing layoutSubviews. But I find that this function is not always called when using autoLayout.
Right now, I'm using the following code where something could possibly cause the view's frame to change:
[self setNeedsLayout];
[self layoutIfNeeded];
and do what I must do in layoutSubviews. But I wonder if this is the best way. Is there a simpler solution, for example a callback function I can use?

You should not have to call both methods at once. Calling setNeedsLayout schedules the view for layouting at the next layouting run, you don't know when this will happen.
On the other hand, layoutIfNeeded layouts the view and all it's subviews immediately. Then layoutSubviews should be called for all the views in the hierarchy starting with the one on which you called layoutIfNeeded.
If you changed your constraints and you need to update your view you should call layoutIfNeeded probably in an animation block so that the views don't "jump", using something like this
// Update constraints ...
[UIView animateWithDuration:0.3 animations:^{
[view layoutIfNeeded];
}];
If you don't change your frame "by hand" (meaning using setFrame:) and update your view only through constraints, after which you call layoutIfNeeded, layoutSubviews should be called every time.
If you do use setFrame:, which you should not do if you are using auto layout, then you can try using an observer.

I think you should check viewDidLayoutSubviews for a callback. Apple's documentation says
Called to notify the view controller that its view has just laid out
its subviews.
Have a closer look here: https://developer.apple.com/library//ios/documentation/UIKit/Reference/UIViewController_Class/index.html
Happy coding!
Z.
Update:
I've answered the wrong question before! :)
For UIViews you can override bounds' setter setBounds (not frame/setFrame, that's a value derived from bounds), and in there you'll get notified of any size change.
Don't forget to call [super setBounds:bounds] in your implementation :)
Z.

Related

Animating constraints changes with setNeedsLayout

The standard way of animating constraint changes is.
// Change constraints like:
someConstraint.constant = 100
UIView.animate(withDuration: 0.25) {
view.layoutIfNeeded()
}
However I was watching the WWDC 2015 session on multitasking on iPad and at the end it said don't use layoutIfNeeded in animation blocks, use setNeedsLayout instead. However I've always thought that this would mean the layout happens later in the run loop and so outside of the animation block. Perhaps it remembers it was called in an animation block?
I tried replacing layoutIfNeeded with setNeedsLayout in my code, and it seemed to work. Is this just coincidence and we actually should be animating autolayout changes with setNeedsLayout?
The documentation for layoutIfNeeded() states:
Use this method to force the view to update its layout immediately. When using Auto Layout, the layout engine updates the position of views as needed to satisfy changes in constraints. Using the view that receives the message as the root view, this method lays out the view subtree starting at the root. If no layout updates are pending, this method exits without modifying the layout or calling any layout-related callbacks.
The documentation for setNeedsLayout() states:
Call this method on your application’s main thread when you want to adjust the layout of a view’s subviews. This method makes a note of the request and returns immediately. Because this method does not force an immediate update, but instead waits for the next update cycle, you can use it to invalidate the layout of multiple views before any of those views are updated. This behavior allows you to consolidate all of your layout updates to one update cycle, which is usually better for performance.
setNeedsLayout() defers the layout for all subviews of the view, till the next cycle of the runloop, thereby consolidating all the changes to the appropriate time, instead of side-stepping the process and forcing the layout to occur instantaneously. Yes you should use setNeedsLayout() instead of layoutIfNeeded() in the UIView.animate function. Be a good citizen if possible.

How to synchronize CALayer and UIView animations up and down a complex hierarchy

See How to manage CALayer animations throughout a hierarchy for a follow-up question that is concerned just with how to synchronize parent-child layer animations.
This is a design question concerning running dependent animations at different levels of the view hierarchy in response to animations up the hierarchy.
I have a container controller that has any number of child controllers. The parent controller organizes this content on the screen and at various points needs to change the size and positions of its children views.
I'm trying to animate each of these child views to transition its size/shape/position from their original starting point to the destination. A basic first pass with some basic animations starts to get the job done.
Things get more complicated though by the fact that some of the views being resized should also be performing animations on the contents of the view. Imagine a child view with centered content. As the child is shrunk or expanded, the centered content should be animated alongside the outer animation to compensate for the bounds changes so that the content stays centered.
To further complicate matters, I also have a mask layer for each child view that needs to be animated alongside the child’s animation. I also have some gestures that require NO animations to be used for transitions - so I need a way to sometimes animate the whole view/layer tree, and sometimes not.
So all of this gives me an architecture where I have something like the following
ContainerViewController.view
-> auxiliary and decorative views
-> WrapperView (multiple)
----> mask layer
-> Child controller view
-> subviews & layers
Now my question is really one of maintainability. I can animate each of these parts using explicit or implicit animations. What I need to figure out is what’s the best way to make sure that all of the animations being done are done using the same duration and timing function. At present, I trigger a lot of these off of property changes. In some cases the property changes come from layoutSubviews (triggered from setNeedsLayout).
So, what’s the best strategy for setting up these animations, especially the explicit ones. Is the best that I can do just picking up values from CATransaction? My fear is that not every property needs to be animated in every case (like in the auxiliary views) - I already am flipping setDisableActions on/off to force/deny some property animations.
Should CATransaction be used to trigger the setup of explicit view animations? How do I bind the parameters specified for a UIView animation to the parameters that will be used for the underlying layers? The following code seems to get the job done, but seems really ugly.
-(void) animateForReason:(enum AnimationReason) animationReason
animations:(void(^)()) animationBlock completion:(void(^)(BOOL)) completionBlock {
const auto animationDuration = 3.0; // make this long to be noticeable!
[UIView animateWithDuration:animationDuration delay:0 options:UIViewAnimationOptionLayoutSubviews
animations:^{
[CATransaction begin];
[CATransaction setAnimationDuration:animationDuration];
animationBlock();
[CATransaction commit];
}completion:completionBlock];
}
I think that UIViewControllerTransitionCoordinator is out because I need to do all of these animations in response to user actions, not just external things, like rotations or frame changes.
There are a few options to consider:
For UIView transitions, you can pass the UIViewAnimationOptionLayoutSubviews option. This will animate the changes between subviews before and after calling layoutSubviews on the view whose frame you just changed.
Instead of using layoutSubviews at all, you could override setBounds: or setFrame: on your UIView and CALayer subclasses. This way, if they're called within an animation block, the subviews will animate together with the superview. If they're not called within an animation block, they'll update instantly.
My fear is that not every property needs to be animated in every case (like in the auxiliary views) - I already am flipping setDisableActions on/off to force/deny some property animations.
Generally, if you want it animated, put it in an animation block, and if you don't, don't. Obviously, it can get more complex than this, which is why Apple sometimes has a setter with an animated argument (like setSelected:animated:).
If you have sometimes on, sometimes off properties, follow this pattern yourself. One possible implementation:
- (void) setNumberOfWidgets:(int)widgetCount animated:(BOOL)animated {
BOOL oldAnimationValue = [UIView areAnimationsEnabled];
if (!animated) {
[UIView setAnimationsEnabled:NO];
}
// a related property which may or may not cause an animation
self.someOtherProperty = someValue;
if (!animated) {
[UIView setAnimationsEnabled:oldAnimationValue];
}
}
My question is really one of maintainability.
Yes, it sounds like you're thinking about some kind of giant object that manages all the animation possibilities for you. Resist this temptation. Let each view be responsible for animating its own subviews, and tell a view (don't let it ask) whether or not these changes should be animated.
I think that UIViewControllerTransitionCoordinator is out because I need to do all of these animations in response to user actions, not just external things, like rotations or frame changes.
Yes, that's correct. Don't use UIViewControllerTransitionCoordinator for this.

View not updating when autolayout constraint modified programmatically

I'm having trouble getting my view to lay itself out properly after modifying a second constraint (the first works fine). I'm modifying one constraint, animating it, and then modifying another constraint in the completion block (without animation).
My reason for doing this is I don't want the second constraint change to affect the animation, but I want it to instantly take effect as soon as the initial animation completes.
The problem is the second constraint change is simply not taking effect, no matter what I try.
Here's what I'm trying (note the problem is only when toolbarVisible is YES, as I perform both animations up front when it is NO):
if ( !toolbarVisible )
self.containerViewBottomSpace.constant = toolbarVisible ? 60.f : 0;
self.toolbarBottomSpace.constant = toolbarVisible ? 0 : 60.f; // this one works fine
[self.view setNeedsUpdateConstraints];
[UIView animateWithDuration:0.3f animations:^{
[self.view layoutIfNeeded];
} completion:^(BOOL finished) {
if ( toolbarVisible )
self.containerViewBottomSpace.constant = 60.f; // this one is not taking effect
[self.view setNeedsUpdateConstraints];
[self.view layoutIfNeeded];
}];
The above method takes place inside an observeValueForKeyPath:... method. I've tried wrapping the whole above code in a dispatch_after block, but that has an even worse effect in that it is always one step behind (i.e. containerView is always in the opposite position as it should be) as if the second constraint modification isn't taking effect until the next animation.
Similarly, if I push another view onto the navigation controller (which is embedded inside the containerView), the layout corrects itself. It's as though it's just ignoring my command to lay itself out again for some reason.
Actually I just noticed that even if I make the second change initially and skip the completion block altogether, it doesn't appear to be animating the second constraint change in all cases. Something is fishy.
Update: I was using dispatch_after incorrectly thinking it accepted an NSTimeInterval. After giving it a proper dispatch_time_t and putting it inside the completion block, the layout is now sometimes updating. However, it's very flaky and this seems very hackish so I feel as though I'm missing something. If anyone knows more about this please enlighten me and I'll give you the answer.
Update 2: I decided to try logging the height of the element I am trying to change in viewDidLayoutSubviews. Interestingly, it gets called twice, first with the previous value and second with the correctly adjusted height. So I guess it is at least partially laying things out correctly, but for some reason the tableView that is inside the containerView is still not resizing itself properly. I'm not sure what I'm missing.
I'd first breakpoint and make sure you're not hitting the animation code a second time during the animation duration. If it's in observeValueForKeyPath you could hit the animation multiple times and it causes some inconsistency.
You also shouldn't need to call setNeedsUpdateConstraints or layoutIfNeeded. Setting the constant should be enough.
I'd also make sure that you've got the constraints wired up correctly and toolbarVisible is true.

Scope UIView animations to specific views or properties

I have a UIView with subviews and want to animate only specific properties of certain views. For example, I sometimes want to call [self layoutIfNeeded] and animate only the bounds but not other properties of the view or its subviews.
The problem is that +[UIView animateWithDuration:animations] tracks subviews and all animatable properties. Is there a reasonable solution to this?
Take a look at +[UIView performWithoutAnimation:]. You specify a block of changes you wish to perform without animation and they happen immediately.
This is good for iOS7 and above, and only for UIKit animation. For dealing with animations on layer objects directly, or support for older versions of iOS, you can use the following code:
[CATransaction begin];
[CATransaction setDisableActions:YES];
//Perform any changes that you do not want to be animated
[CATransaction commit];
More on performWithoutAnimation: here and on setDisabledActions: here.
If you do not wish to alter the parent's code, you can implement the setter methods of the properties you do not wish animated, and wrap the super call with performWithoutAnimation:, like so:
- (void)setFrame:(CGRect)frame
{
[UIView performWithoutAnimation: ^ {
[super setFrame:frame];
}];
}
If you don't want the subviews to resize when the parent view bounds change, you should set each of the subviews' autoresizingMask appropriately and/or set the parent's autoresizesSubviews flag.
Otherwise you have to override the parent's layoutSubviews and modify each of the subviews' frames back to their previous values.
Since you want to do something a bit more complex than just moving a view I would absolutely suggest you step down from UIKit to Core Animation. This will allow you to create animations on specific key paths for each object.
I promise it's not that scary, by creating a few CABasicAnimations and grouping them together in a CAAnimationGroup you can add them to any UIView's CALayer. This will definitely provide the control you need.

How to react to UIControl resize when its data changes

I've built a UIControl subclass to display a 1-month calendar view on
an iPhone. Most months require 5 weeks to display the dates, but
some months need 6, so I've built the control to dynamically resize
itself as needed. I'm using UIView animation to change the frame of
the control here.
The problem is, I now need the other controls on the screen to
move/resize when the calendar changes size. And I really need that
to happen with the animation of the calendar control changing size.
Ideally, I'd do this without coding a bunch of details in my calendar
control about other controls on the screen.
What's the best strategy here? I was hoping I could somehow anchor
the other controls to the frame of the calendar control and have the
platform adjust their location/size as it animates the frame change.
But, thus far, I can't find any combination of struts and springs to
make that happen.
Do I just need to bite the bullet and add my other on-screen controls
to the animation happening inside my calendar control?
I'll be interested to see if there are better answers to this.
At this point, all I know to do is override setFrame on your calendar view and when it changes, send setNeedsLayout to its superview.
But I'm not sure if standard views will autoresize this correctly. Generally geometry flows down the view tree, not up, and you want it to do both.
You may have to implement layoutSubviews on the containing view.
Move the animation logic out of the specific view and into a view controller that manages all of the controls. The view can still figure out its own proper size, and let the view controller ask. For instance:
[self.calendarView setDate:date];
CGSize calendarSize = [self.calendarView sizeForDate:date];
[UIView animateWithDuration:...
animations:^{
... re-layout everything, including the calendarView ...
}];
Another, similar approach:
[self.calendarView setDate:date];
[UIView animateWithDuration:...
animations:^{
[self.calendarView sizeToFit];
... re-layout everything else ...
}];
There are lots of similar approaches based on your preferences. But the key is to move the logic out of the specific view. Usually a view controller is the best solution. If the controls make a logical collection that could go into a single UIView, then you could have that "collection" UIView manage the same thing in its layoutSubviews, but it's more common that a view controller is a better solution here.
Thanks for the help. I tried both of these approaches with success. The override of setFrame and implementation of layoutSubviews worked, but the other controls jumped to their new locations rather than animating to those locations as the calendar control grew.
The approach of moving the other controls during the animation is what I had to go with. However, I wanted to keep the logic of the control pretty self-contained for re-use, so I left the animation in the control but added a delegate to react to size change, and put the delegate call inside the animation block. In full disclosure, I am also animating the new month's calendar onto the control while I'm growing the control, and this way I could do both of those things in the animation block.
So, the end code looks something like this:
// inside the calendar control:
[UIView animateWithDuration:0.5 animations:^{
self.frame = newOverallFrame; // animate the growth of the control
_offScreenCalendar.frame = newOnScreenFrame; // animate new month on screen
_onScreenCalendar.frame = newOffScreenFrame; // animate old month off screen
if (self.delegate)
{
[self.delegate handleCalendarControlSizeChange:newOverallFrame]; // animate other controls per the delegate
}
}];

Resources