Scope UIView animations to specific views or properties - ios

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.

Related

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

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.

What is the default animation duration for UICollectionView layout changes?

Say I do this:
[self.collectionView setCollectionViewLayout:myNewLayout animated:YES];
What is the duration of the animation that happens?
My application of this is that I'm animating layer properties inside the cells when this changes, and because layer animations don't get caught by UIView's block-based animations, I have to do it separately with my own animation block.
It you know you're inside a UIView animation, then [CATransaction animationDuration] will give you what you need.
Swift
Use this:
CATransaction.animationDuration()
- Note:
You may need to check out and apply the timing curve too. Because it affects how the animation looks
Use this if so: CATransaction.animationTimingFunction()

Why do docs indicate CALayer animations must be in UIView animation blocks?

I am currently reading Apple's Core Animation Guide, where I found the following passage regarding layer-backed views in iOS:
If you want to use Core Animation classes to initiate animations, you must issue all of your Core Animation calls from inside a view-based animation block. The UIView class disables layer animations by default but reenables them inside animation blocks. So any changes you make outside of an animation block are not animated.
Just below the quote, the documentation includes the following code listing:
[UIView animateWithDuration:1.0 animations:^{
// Change the opacity implicitly.
myView.layer.opacity = 0.0;
// Change the position explicitly.
CABasicAnimation* theAnim = [CABasicAnimation animationWithKeyPath:#"position"];
theAnim.fromValue = [NSValue valueWithCGPoint:myView.layer.position];
theAnim.toValue = [NSValue valueWithCGPoint:myNewPosition];
theAnim.duration = 3.0;
[myView.layer addAnimation:theAnim forKey:#"AnimateFrame"];
}];
which implies that both implicit and explicit animations on CALayers backing UIViews must be placed within an animation block.
However, I have found this to be patently untrue. Specifically, I have successfully implemented explicit animations using Core Animation classes outside of a UIView animation block.
Have I misunderstood this passage, or is it out-of-date, or something else?
Some additional notes:
I assume that "the UIView class disables layer animations by default but reenables them inside animation blocks" refers to the +[UIView setAnimationsEnabled:] method. When I get back to a computer that can do so, I'll check to see whether +[UIView areAnimationsEnabled] returns YES or NO.
That quote refers to the layer that is backing the view. It is not true for stand-alone layers that you create and manage yourself.
Every view on iOS is backed by a layer. When you change the view's properties, it changes the underlying layer property. By default the layer would have implicit animations, but the layer "disables" that behavior except for when you are inside of an UIView animation block. This is what that part of the documentation is referring to.
There are a couple of ways you can use Core Animation to animate a layer property. The most common is to add the animation object to the layer when you want to animate the property, but you can also make customizations through the actions dictionary and the styles dictionary if you always want to animate when a property changes. The last two would also be disabled for the layer that is backing a view.

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.

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