Autolayout and shadow - ios

I've got a problem with adding shadow to my UIView which is created in iOS 6 application with Autolayout.
Let's assume I have a method that adds a shadow on the bottom of UIView (this is actually a Category of UIView, so it's reusable):
- (void) addShadowOnBottom {
self.layer.shadowOffset = CGSizeMake(0, 2);
self.layer.shadowOpacity = 0.7;
self.layer.shadowColor = [[UIColor blackColor] CGColor];
self.layer.shadowPath = [UIBezierPath bezierPathWithRect:self.bounds].CGPath;
}
When I call this method in viewDidLoad of some UIViewController, shadow is not added, probably due to all constraints, that have to be calculated.
When I call this method in viewWillAppear the same situation.
When I call this method in viewDidAppear it works, but when new view shows up there is a short moment when there is no shadow and it appears after a while.
If I resign from setting the shadowPath and remove line self.layer.shadowPath everything works, but view transitions are not smooth.
So my question is what is the right way to add a shadow to view in iOS 6 with Autolayout turned on ?

Another thing you can add to the layer when working with AutoLayout and you need a shadow on a UIView where the frame is not yet known is this :
self.layer.rasterizationScale = [[UIScreen mainScreen] scale]; // to define retina or not
self.layer.shouldRasterize = YES;
Then remove the shadowPath property because the auto layout constraints are not yet processed, so it's irrelevant. Also at the time of execution you will not know the bounds or the frame of the view.
This improves performance a lot!

i removed self.layer.shadowPath = [UIBezierPath bezierPathWithRect:self.bounds].CGPath;
from your code and it is working for me in viewDidLoad, please confirm.
Increasing shdowOffset will make you see the shadow more clear.

Having the exact same issue...
Although I am unable to get the CALayer shadow on a view to animate nicely, at least the shadow does re-align properly after animation.
My solution (which works fine in my application) is the set the shadowOpacity to 0, then reset it to the desired value AFTER the animation has completed. From a user's perspective, you cannot even tell the shadow is gone because the animations are typically too fast to perceive the difference.
Here is an example of some code in my application, in which I am changing the constant value of a constraint, which is 'trailing edge to superview' NSLayoutContraint:
- (void) expandRightEdge
{
[self.mainNavRightEdge setConstant:newEdgeConstant];
[self updateCenterContainerShadow];
[UIView animateWithDuration:ANIMATION_DURATION delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
[[NSNotificationCenter defaultCenter] postNotificationName:#"PanelLayoutChanged" object:nil];
[self.view layoutIfNeeded];
} completion:^(BOOL finished) {
nil;
}];
}
- (void) updateCenterContainerShadow
{
self.centerContainer.layer.masksToBounds = NO;
self.centerContainer.layer.shadowOpacity = 0.8f;
self.centerContainer.layer.shadowRadius = 5.0f;
self.centerContainer.layer.shadowOffset = CGSizeMake(0, 0);
self.centerContainer.layer.shadowColor = [UIColor blackColor].CGColor;
CGPathRef shadowPath = [UIBezierPath bezierPathWithRect:self.centerContainer.layer.bounds].CGPath;
[self.centerContainer.layer setShadowPath:shadowPath];
// Schedule a time to fade the shadow back in until we can figure out the CALayer + Auto-Layout issue
[self performSelector:#selector(fadeInShadow) withObject:nil afterDelay:ANIMATION_DURATION+.05];
}
- (void) fadeInShadow
{
[self.centerContainer.layer setShadowOpacity:0.8f];
}
Two things:
I could have put the fadeInShadow in the completion block, but due to the way some of my other code is factored, this works better for me.
I realize I am not performing a fade in with "fadeInShadow", but given how quickly it renderes after the completion of the animation, I found it is not necessary.
Hope that helps!

Related

When should I be setting borders in UIViews?

I've got a UIView that does not fill the whole screen and I would like to add a top border to that view. However, I keep getting the following:
Here is the code I am using:
CGFloat thickness = 4.0f;
CALayer *topBorder = [CALayer layer];
topBorder.frame = CGRectMake(0, 0, self.announcementCard.frame.size.width, thickness);
topBorder.backgroundColor = [UIColor blueColor].CGColor;
How I know why the border goes off the screen. This is because I put the border on the view inside the UIViews init method. When I do this the self.announcementCard.frame.size.width is 1000 and hence why the border goes off the screen. The self.announcementCard.frame.size.width has a width and height of 1000. The reason for this is because the UIView hasn't added the constraints to the UIView in its init methods.
Thus, my question is when should I be calling the code I've written above? When will self.announcementCard.frame.size.width have its constraints added to it and have its frame updated?
You should add your subviews (or sublayers) in the viewDidLoad method. However if you are using the auto-layout keep a reference of your sublayer and update it in the viewDidLayoutSubviews method:
- (void)viewDidLoad {
[super viewDidLoad];
_borderLayer = [CALayer layer];
[self.view.layer addSublayer:_borderLayer];
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
_borderLayer.frame = CGRectMake(0, 0, self.view.bounds.size.width, 3);
}
Otherwise you can simply clipsToBounds the view to avoid the subviews to be visible beyond the bounds.
self.view.clipsToBounds = YES;
in the init the graphics isn't made yet. You have to put all your configuration on graphics object in the viewDidLoad: or viewWillAppear: of the UIViewController.
Short Answer:
viewWillAppear
By the time viewWillAppear is called, your subviews have been laid out and the frames are valid. Doing frame-based calculations in viewDidLoad can often have issues since the frames have not been set.

How to draw a border around the content of a UIScrollView?

I'm working on a document viewer. The document is displayed inside a UIScrollView so that it can be scrolled and zoomed. I need to draw a border around the document in order to separate it visually from the background of the UIScrollView. The border must not be zoomed together with the document -- it should maintain a constant thickness regardless of the zoom scale.
My current setup consists of a UIScrollView with two UIView children -- one for the document, and one for the border. I've overriden viewForZoomingInScrollView: to return the document view. I've also overridden layoutSubviews to center the document view (in case it's smaller than the UIScrollView) and then resize and position the border view behind it so that it looks like a frame. This works OK when the user is scrolling and zooming manually. But when I use zoomToRect:animated: to zoom programatically, layoutSubviews is called before the animation starts and my border view gets resized immediately with the document view catching up a bit later.
Clarification: The border needs to be tightly fitting around the document view and not around the UIScrollView itself.
Sample Code :
yourScrollView.layer.cornerRadius=8.0f;
yourScrollView.layer.masksToBounds=YES;
yourScrollView.layer.borderColor=[[UIColor redColor]CGColor];
yourScrollView.layer.borderWidth= 1.0f;
Don't Forget : #Import <QuartzCore/QuartzCore.h>
First you need to Import QuartzCore framework to your App.
then import that .h file on which class where you want to set the border.
like this.
#import <QuartzCore/QuartzCore.h>
Setup for Border.
ScrollView.layer.cornerRadius=5.0f;
ScrollView.layer.masksToBounds=YES;
ScrollView.layer.borderColor=[[UIColor redColor]CGColor];
ScrollView.layer.borderWidth= 4.0f;
check this one really helpful to you.
Finally, I was able to fix the problem with animated zooming. My setup is the same as described in the question. I just added some code to my layoutSubview implementation in order to detect any running UIScrollView animation and match it with a similar animation for resizing the border.
Here is the code:
- (void)layoutSubviews
{
[super layoutSubviews];
// _pageView displays the document
// _borderView represents the border around it
. . .
// _pageView is now centered -- we have to move/resize _borderView
// layers backing a view are not implicitly animated
// the following two lines work just fine if we don't need animation
_borderView.layer.bounds = CGRectMake(0.0, 0.0, frameToCenter.size.width + 2.0 * 15.0, frameToCenter.size.height + 2.0 * 15.0);
_borderView.layer.position = _pageView.center;
if (_pageView.layer.animationKeys.count > 0)
{
// UIScrollView is animating its content (_pageView)
// so we need to setup a matching animation for _borderView
[CATransaction begin];
CAAnimation *animation = [_pageView.layer animationForKey:[_pageView.layer.animationKeys lastObject]];
CFTimeInterval beginTime = animation.beginTime;
CFTimeInterval duration = animation.duration;
if (beginTime != 0.0) // 0.0 means the animation starts now
{
CFTimeInterval currentTime = [_pageView.layer convertTime:CACurrentMediaTime() fromLayer:nil];
duration = MAX(beginTime + duration - currentTime, 0.0);
}
[CATransaction setAnimationDuration:duration];
[CATransaction setAnimationTimingFunction:animation.timingFunction];
// calculate the initial state for _borderView animation from _pageView presentation layer
CGPoint presentationPos = [_pageView.layer.presentationLayer position];
CGRect presentationBounds = [_pageView.layer.presentationLayer frame];
presentationBounds.origin = CGPointZero;
presentationBounds.size.width += 2.0 * 15.0;
presentationBounds.size.height += 2.0 * 15.0;
CABasicAnimation *boundsAnim = [CABasicAnimation animationWithKeyPath:#"bounds"];
boundsAnim.fromValue = [NSValue valueWithCGRect:presentationBounds];
boundsAnim.toValue = [NSValue valueWithCGRect:_borderView.layer.bounds];
[_borderView.layer addAnimation:boundsAnim forKey:#"bounds"];
CABasicAnimation *posAnim = [CABasicAnimation animationWithKeyPath:#"position"];
posAnim.fromValue = [NSValue valueWithCGPoint:presentationPos];
posAnim.toValue = [NSValue valueWithCGPoint:_borderView.layer.position];
[_borderView.layer addAnimation:posAnim forKey:#"position"];
[CATransaction commit];
}
}
It looks hacky but it works. I wish I didn't have to reverse engineer UIScrollView in order to make a simple border look good during animation...
Import QuartzCore framework:
#import <QuartzCore/QuartzCore.h>
Now Add color to its view:
[scrollViewObj.layer setBorderColor:[[UIColor redColor] CGColor]];
[scrollViewObj.layer setBorderWidth:2.0f];

Can't get a CALayer to update its drawLayer: DURING a bounds animation

I'm trying to animate a custom UIView's bounds while also keeping its layer the same size as its parent view. To do that, I'm trying to animate the layers bounds alongside its parent view. I need the layer to call drawLayer:withContext AS its animating so my custom drawing will change size correctly along with the bounds.
drawLayer is called correctly and draws correctly before I start the animation. But I can't get the layer to call its drawLayer method on EACH step of the bounds animation. Instead, it just calls it ONCE, jumping immediately to the "end bounds" at the final frame of the animation.
// self.bg is a property pointing to my custom UIView
self.bg.layer.needsDisplayOnBoundsChange = YES;
self.bg.layer.mask.needsDisplayOnBoundsChange = YES;
[UIView animateWithDuration:2 delay:0 options:UIViewAnimationOptionCurveEaseOut|UIViewAnimationOptionAutoreverse|UIViewAnimationOptionRepeat animations:^{
[CATransaction begin];
self.bg.layer.bounds = bounds;
self.bg.layer.mask.bounds = bounds;
[CATransaction commit];
self.bg.bounds = bounds;
} completion:nil];
Why doesn't the bounds report a change AS its animating (not just the final frame)? What am I doing wrong?
This might or might not help...
Many people are unaware that Core Animation has a supercool feature that allows you to define your own layer properties in such a way that they can be animated. An example I use is to give a CALayer subclass a thickness property. When I animate it with Core Animation...
CABasicAnimation* ba = [CABasicAnimation animationWithKeyPath:#"thickness"];
ba.toValue = #10.0f;
ba.autoreverses = YES;
[lay addAnimation:ba forKey:nil];
...the effect is that drawInContext: or drawLayer:... is called repeatedly throughout the animation, allowing me to change repeatedly the way the layer is drawn in accordance with its current thickness property value at each moment (an intermediate value in the course of the animation).
It seems to me that that might be the sort of thing you're after. If so, you can find a working downloadable example here:
https://github.com/mattneub/Programming-iOS-Book-Examples/tree/master/ch17p498customAnimatableProperty
Discussion (from my book) here:
http://www.apeth.com/iOSBook/ch17.html#_making_a_property_animatable
This is because the layer you are drawing to is not the same layer as the one displayed on the screen.
When you animate a layer property it will immediately be set to its final value in the model layer, (as you have noticed), and the actual animation is done in the presentation layer.
You can access the presentation layer and see the actual values of the animated properties:
CALayer *presentationLayer = (CALayer *)[self.bg.layer presentationLayer];
...
Since you haven't provided your drawLayer:withContext method, it's unclear what you want to draw during the animation, but if you want to animate custom properties, here is a good tutorial for doing that.
Firstly, the layer of a layer backed (or hosting) view is always resized to fit the bounds of its parent view. If you set the view to be the layers delegate then the view will receive drawLayer:inContext: at each frame. Of course you must ensure that If your layer has needsDisplayOnBoundsChange == YES.
Here is an example (on the Mac) of resizing a window, which then changes the path of the underlying layer.
// My Nib contains one view and one button.
// The view has a MPView class and the button action is resizeWindow:
#interface MPView() {
CAShapeLayer *_hostLayer;
CALayer *_outerLayer;
CAShapeLayer *_innerLayer;
}
#end
#implementation MPView
- (void)awakeFromNib
{
[CATransaction begin];
[CATransaction setDisableActions:YES];
_hostLayer = [CAShapeLayer layer];
_hostLayer.backgroundColor = [NSColor blackColor].CGColor;
_hostLayer.borderColor = [NSColor redColor].CGColor;
_hostLayer.borderWidth = 2;
_hostLayer.needsDisplayOnBoundsChange = YES;
_hostLayer.delegate = self;
_hostLayer.lineWidth = 4;
_hostLayer.strokeColor = [NSColor greenColor].CGColor;
_hostLayer.needsDisplayOnBoundsChange = YES;
self.layer = _hostLayer;
self.wantsLayer = YES;
[CATransaction commit];
[self.window setFrame:CGRectMake(100, 100, 200, 200) display:YES animate:NO];
}
- (void) drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
if (layer == _hostLayer) {
CGSize size = layer.bounds.size;
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, 0, 0);
CGPathAddLineToPoint(path, NULL, size.width, size.height);
_hostLayer.path = path;
CGPathRelease(path);
}
}
- (IBAction)resizeWindow:(id)sender
{
[self.window setFrame:CGRectMake(100, 100, 1200, 800) display:YES animate:YES];
}
#end

How to fade UIView background color - iOS 5

I'm having trouble trying to get a custom UIView class to fade it's background. I've checked out the following StackOverflow questions but they don't seem to work for me.
Fade background-color from one color to another in a UIView
How do you explicitly animate a CALayer's backgroundColor?
So I have a custom UIView where users can draw things. If what they draw is incorrect, I want to make the background color fade to red then back to white.
I have this custom method in the custom UIView called
- (void)indicateMistake;
When I call that method I want it to perform the background color animation, so i have this in the method:
CABasicAnimation* fade = [CABasicAnimation animationWithKeyPath:#"backgroundColor"];
fade.fromValue = (id)[UIColor whiteColor].CGColor;
fade.toValue = (id)[UIColor redColor].CGColor;
[fade setDuration:3];
[self.layer addAnimation:fade forKey:#"fadeAnimation"];
But nothing seems to happen when I do that.
So then I tried a silly rotation animation to see if it even works:
CABasicAnimation* rotate = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
rotate.toValue = [NSNumber numberWithInt:M_PI];
[rotate setDuration:1];
[self.layer addAnimation:rotate forKey:#"rotateAnimation"];
For some reason that works and the custom UIView rotates.
Then reading more StackOverflow answers I tried this:
[UIView animateWithDuration:3 animations:^{
[self setBackgroundColor: [UIColor redColor]];
} completion:^(BOOL finished) {
[self setBackgroundColor: [UIColor whiteColor]];
}];
That changes the color from red then back to white immediately. Sometimes It's so fast I sometimes can't see it happen. If i comment out the [self setBackgroundColor: [UIColor whiteColor]]; it stays red. But There's node gradual white to red effect.
I've ran out of ideas.. Any help would be appreciated!
[UIView animateWithDuration:0.3f animations:^{
fade.layer.backgroundColor = [UIColor redColor].CGColor;
}
That assumes you have pre-set your fade view to the white color that you want prior.
The reason your last attempt at an animation jumps back to white is because in your completion clause of animateWithDuration, you're setting the view back to white.
That completion clause executes when the animation is finished, so the animation (white-red) occurs, then your compiler goes to complete and jumps the fade view back to white.
As an added bonus:
I'm sure you might be wondering how you can go back to white.
Well, lets assume that the pressing of a UIButton calls this function called, "animateColors". So with that said, simply implement a BOOL.
- (void)animateColors {
if (!isRed) {
[UIView animateWithDuration:0.3f animations:^{
fade.layer.backgroundColor = [UIColor redColor].CGColor;
}];
isRed = YES;
} else if (isRed) {
[UIView animateWithDuration:0.3f animations:^{
fade.layer.backgroundColor = [UIColor whiteColor].CGColor;
}];
isRed = NO;
}
}
Obviously, you're going to want to make a property declaration of the boolean, isRed, in you interface or header file.
So I have a custom UIView where users can draw things.
I'm assuming that means you've implemented -drawRect:.
IIRC, by default (if UIView.clearsContextBeforeDrawing is set), the context gets filled with the view's background colour, -drawRect: gets called, and the resulting bitmap is drawn on screen instead of the background colour. Animating this would mean animating between the two bitmaps, which isn't something Core Animation is particularly good at. (I could be wrong about the details here, but it explains the behaviour).
The easiest fix is to set the background colour to [UIColor clearColor] and animate the background colour of a view behind the one you are drawing to. This means the view will not need to be redrawn, only recomposited over the "background".
I tried your code, as is. It worked perfectly - it faded from my view's original white to red, slowly (and then jumped back to white, which is what you'd expect). So if this isn't working for you, something else is going on.
A very useful diagnostic tool is to extract your troublesome code and make a project consisting of nothing else. What I did was what you should do. Make a completely new Single View Application project. Now you have a view controller and its view. Implement this method:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
CABasicAnimation* fade = [CABasicAnimation animationWithKeyPath:#"backgroundColor"];
fade.fromValue = (id)[UIColor whiteColor].CGColor;
fade.toValue = (id)[UIColor redColor].CGColor;
[fade setDuration:3];
[self.view.layer addAnimation:fade forKey:#"fadeAnimation"];
}
Run the project. The fade works perfectly. So you see, whatever is going wrong is not in this code! It is somewhere else. Perhaps you have other code that is also doing things to the color - a different animation that is crushing this one. Perhaps some reference is not referring to what you think it is. Perhaps something is covering the layer so you can't see it (a sublayer). Who knows? You didn't show your whole project (nor should you). The important thing is to prove to yourself that the code should work so you can figure out what's really wrong. And I've shown you how to do exactly that.

How do I create a smoothly resizable circular UIView?

I'm trying to create a UIView which shows a semitransparent circle with an opaque border inside its bounds. I want to be able to change the bounds in two ways - inside a -[UIView animateWithDuration:animations:] block and in a pinch gesture recogniser action which fires several times a second. I've tried three approaches based on answers elsewhere on SO, and none are suitable.
Setting the corner radius of the view's layer in layoutSubviews gives smooth translations, but the view doesn't stay circular during animations; it seems that cornerRadius isn't animatable.
Drawing the circle in drawRect: gives a consistently circular view, but if the circle gets too big then resizing in the pinch gesture gets choppy because the device is spending too much time redrawing the circle.
Adding a CAShapeLayer and setting its path property in layoutSublayersOfLayer, which doesn't animate inside UIView animations since path isn't implicitly animatable.
Is there a way for me to create a view which is consistently circular and smoothly resizable? Is there some other type of layer I could use to take advantage of the hardware acceleration?
UPDATE
A commenter has asked me to expand on what I mean when I say that I want to change the bounds inside a -[UIView animateWithDuration:animations:] block. In my code, I have a view which contains my circle view. The circle view (the version that uses cornerRadius) overrides -[setBounds:] in order to set the corner radius:
-(void)setBounds:(CGRect)bounds
{
self.layer.cornerRadius = fminf(bounds.size.width, bounds.size.height) / 2.0;
[super setBounds:bounds];
}
The bounds of the circle view are set in -[layoutSubviews]:
-(void)layoutSubviews
{
// some other layout is performed and circleRadius and circleCenter are
// calculated based on the properties and current size of the view.
self.circleView.bounds = CGRectMake(0, 0, circleRadius*2, circleRadius*2);
self.circleView.center = circleCenter;
}
The view is sometimes resized in animations, like so:
[UIView animateWithDuration:0.33 animations:^(void) {
myView.frame = CGRectMake(x, y, w, h);
[myView setNeedsLayout];
[myView layoutIfNeeded];
}];
but during these animations, if I draw the circle view using a layer with a cornerRadius, it goes funny shapes. I can't pass the animation duration in to layoutSubviews so I need to add the right animation within -[setBounds].
As the section on Animations in the "View Programming Guide for iOS" says
Both UIKit and Core Animation provide support for animations, but the level of support provided by each technology varies. In UIKit, animations are performed using UIView objects
The full list of properties that you can animate using either the older
[UIView beginAnimations:context:];
[UIView setAnimationDuration:];
// Change properties here...
[UIView commitAnimations];
or the newer
[UIView animateWithDuration:animations:];
(that you are using) are:
frame
bounds
center
transform (CGAffineTransform, not the CATransform3D)
alpha
backgroundColor
contentStretch
What confuses people is that you can also animate the same properties on the layer inside the UIView animation block, i.e. the frame, bounds, position, opacity, backgroundColor.
The same section goes on to say:
In places where you want to perform more sophisticated animations, or animations not supported by the UIView class, you can use Core Animation and the view’s underlying layer to create the animation. Because view and layer objects are intricately linked together, changes to a view’s layer affect the view itself.
A few lines down you can read the list of Core Animation animatable properties where you see this one:
The layer’s border (including whether the layer’s corners are rounded)
There are at least two good options for achieving the effect that you are after:
Animating the corner radius
Using a CAShapeLayer and animating the path
Both of these require that you do the animations with Core Animation. You can create a CAAnimationGroup and add an array of animations to it if you need multiple animations to run as one.
Update:
Fixing things with as few code changes as possible would be done by doing the corner radius animation on the layer at the "same time" as the other animations. I put quotations marks around same time since it is not guaranteed that animations that are not in the same group will finish at exactly the same time. Depending on what other animations you are doing it might be better to use only basic animations and animations groups. If you are applying changes to many different views in the same view animation block then maybe you could look into CATransactions.
The below code animates the frame and corner radius much like you describe.
UIView *circle = [[UIView alloc] initWithFrame:CGRectMake(30, 30, 100, 100)];
[[circle layer] setCornerRadius:50];
[[circle layer] setBorderColor:[[UIColor orangeColor] CGColor]];
[[circle layer] setBorderWidth:2.0];
[[circle layer] setBackgroundColor:[[[UIColor orangeColor] colorWithAlphaComponent:0.5] CGColor]];
[[self view] addSubview:circle];
CGFloat animationDuration = 4.0; // Your duration
CGFloat animationDelay = 3.0; // Your delay (if any)
CABasicAnimation *cornerRadiusAnimation = [CABasicAnimation animationWithKeyPath:#"cornerRadius"];
[cornerRadiusAnimation setFromValue:[NSNumber numberWithFloat:50.0]]; // The current value
[cornerRadiusAnimation setToValue:[NSNumber numberWithFloat:10.0]]; // The new value
[cornerRadiusAnimation setDuration:animationDuration];
[cornerRadiusAnimation setBeginTime:CACurrentMediaTime() + animationDelay];
// If your UIView animation uses a timing funcition then your basic animation needs the same one
[cornerRadiusAnimation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
// This will keep make the animation look as the "from" and "to" values before and after the animation
[cornerRadiusAnimation setFillMode:kCAFillModeBoth];
[[circle layer] addAnimation:cornerRadiusAnimation forKey:#"keepAsCircle"];
[[circle layer] setCornerRadius:10.0]; // Core Animation doesn't change the real value so we have to.
[UIView animateWithDuration:animationDuration
delay:animationDelay
options:UIViewAnimationOptionCurveEaseInOut
animations:^{
[[circle layer] setFrame:CGRectMake(50, 50, 20, 20)]; // Arbitrary frame ...
// You other UIView animations in here...
} completion:^(BOOL finished) {
// Maybe you have your completion in here...
}];
With many thanks to David, this is the solution I found. In the end what turned out to be the key to it was using the view's -[actionForLayer:forKey:] method, since that's used inside UIView blocks instead of whatever the layer's -[actionForKey] returns.
#implementation SGBRoundView
-(CGFloat)radiusForBounds:(CGRect)bounds
{
return fminf(bounds.size.width, bounds.size.height) / 2;
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor clearColor];
self.opaque = NO;
self.layer.backgroundColor = [[UIColor purpleColor] CGColor];
self.layer.borderColor = [[UIColor greenColor] CGColor];
self.layer.borderWidth = 3;
self.layer.cornerRadius = [self radiusForBounds:self.bounds];
}
return self;
}
-(void)setBounds:(CGRect)bounds
{
self.layer.cornerRadius = [self radiusForBounds:bounds];
[super setBounds:bounds];
}
-(id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event
{
id<CAAction> action = [super actionForLayer:layer forKey:event];
if ([event isEqualToString:#"cornerRadius"])
{
CABasicAnimation *boundsAction = (CABasicAnimation *)[self actionForLayer:layer forKey:#"bounds"];
if ([boundsAction isKindOfClass:[CABasicAnimation class]] && [boundsAction.fromValue isKindOfClass:[NSValue class]])
{
CABasicAnimation *cornerRadiusAction = [CABasicAnimation animationWithKeyPath:#"cornerRadius"];
cornerRadiusAction.delegate = boundsAction.delegate;
cornerRadiusAction.duration = boundsAction.duration;
cornerRadiusAction.fillMode = boundsAction.fillMode;
cornerRadiusAction.timingFunction = boundsAction.timingFunction;
CGRect fromBounds = [(NSValue *)boundsAction.fromValue CGRectValue];
CGFloat fromRadius = [self radiusForBounds:fromBounds];
cornerRadiusAction.fromValue = [NSNumber numberWithFloat:fromRadius];
return cornerRadiusAction;
}
}
return action;
}
#end
By using the action that the view provides for the bounds, I was able to get the right duration, fill mode and timing function, and most importantly delegate - without that, the completion block of UIView animations didn't run.
The radius animation follows that of the bounds in almost all circumstances - there are a few edge cases that I'm trying to iron out, but it's basically there. It's also worth mentioning that the pinch gestures are still sometimes jerky - I guess even the accelerated drawing is still costly.
Starting in iOS 11, UIKit animates cornerRadius if you change it inside an animation block.
The path property of a CAShapeLayer isn't implicitly animatable, but it is animatable. It should be pretty easy to create a CABasicAnimation that changes the size of the circle path. Just makes sure that the path has the same number of control points (e.g. changing the radius of a full-circle arc.) If you change the number of control points, things get really strange. "Results are undefined", according to the documentaiton.

Resources