How to scale transition UILabel without using a lot of memory? - ios

I'm trying to reproduce some of the 2D transitions in Impress.js's sample presentation in Objective C. Specifically the rotate, pan, and scaling - right now I'm focusing on the scaling.
I've tried scaling a UILabel to the point where it "passes the screen", as the "visualize your Big thoughts" -> "and tiny ideas" does in the sample presentation.
This is what I've tried so far:
UILabel *label = [[UILabel alloc] init];
label.text = #"Hello World!";
label.textColor = [UIColor blackColor];
label.font = [UIFont fontWithName:#"Arial" size:18.f];
[label sizeToFit];
label.center = CGPointMake(self.view.bounds.size.width / 2, self.view.bounds.size.height / 2);
[self.view addSubview:label];
label.contentScaleFactor *= 80;
[UIView animateWithDuration:5 animations:^{
label.transform = CGAffineTransformScale(label.transform, 80, 80);
}];
Unfortunately this eats up about ~30-60 MB of RAM, depending on what the contentScaleFactor and initial font size is. If I don't increase the contentScaleFactor the text looks blurry. Increasing the font size also seems to eat just as much memory.
Here is what it looks in the profiler:
And this is just a single UILabel.
Is there any way to do this without eating up so much memory, without sacrificing quality of the text being rendered or the transitions?

Project download link
I don't believe it's necessary to leave Quartz to achieve this reproduction. Everything you've described as well as everything I've gathered from messing with Impress.js seems to be replicable through applying transforms (mostly 2D, some 3D) to a set of UILabels added to a container view that can be moved freely within the main view.
To do this, the project I created uses a subclass of UILabel titled "ImpressLabel" with an extra init function where instead of passing the label a frame, you pass it a size, a center point, and a CGFloat for the label's rotation on the Z axis. This transform is applied to the label upon instantiation so that when you set up the labels they will appear on screen already in the position and transformation you specify.
Then as far as configuring the text goes, you can pass the label an NSAttributedString instead of a NSString. This allows you to modify different parts of the string independently, so different words in the label can be different sizes, fonts, colors, background colors, etc. Here's an example of the above two paragraphs:
ImpressLabel *label1 = [[ImpressLabel alloc] initWithSize:CGSizeMake(260.0f, 80.0f) andCenterPointInSuperview:CGPointMake(500.0f, 500.0f) andRotationInSuperview:0.0f andEndingScaleFactor:1.3];
NSMutableAttributedString *firstLabelAttributes = [[NSMutableAttributedString alloc] initWithString:#"then you should try\nimpress.js*\n* no rhyme intended"];
[firstLabelAttributes addAttribute:NSFontAttributeName
value:[UIFont systemFontOfSize:label1.font.pointSize - 2]
range:NSMakeRange(0, 19)];
[firstLabelAttributes addAttribute:NSFontAttributeName
value:[UIFont systemFontOfSize:label1.font.pointSize - 8]
range:NSMakeRange(firstLabelAttributes.string.length - 19, 19)];
[firstLabelAttributes addAttribute:NSFontAttributeName
value:[UIFont boldSystemFontOfSize:label1.font.pointSize + 14]
range:NSMakeRange(23, 11)];
[label1 setNumberOfLines:3];
[label1 setAttributedText:firstLabelAttributes];
[label1 setTextAlignment:NSTextAlignmentCenter];
[containmentView addSubview:label1];
Now, on to more of the guts of the whole operation. As I mentioned above, this subclass adds a tap gesture to each label. When a tap is recognized a few things happen. The view containing the labels will pan/back/away by adjusting it's scale. It will also begin rotate and adjust its anchor point in the main view so that when the animation stops, the selected label will be centered on screen in the correct orientation. Then of course, while this is all going on the alpha of the selected label will be brought up to 1.0f while the alpha of the rest will be lowered to 0.25f.
- (void)tapForRotationDetected:(UITapGestureRecognizer *)sender {
CABasicAnimation *scale = [CABasicAnimation animationWithKeyPath:#"transform.scale"];
[scale setToValue:[NSNumber numberWithFloat:0.8]];
[scale setAutoreverses:YES];
[scale setDuration:0.3];
//Create animation to adjust the container views anchorpoint.
CABasicAnimation *adjustAnchor = [CABasicAnimation animationWithKeyPath:#"anchorPoint"];
[adjustAnchor setFromValue:[NSValue valueWithCGPoint:self.superview.layer.anchorPoint]];
[adjustAnchor setToValue:[NSValue valueWithCGPoint:CGPointMake(self.center.x / self.superview.frame.size.width, self.center.y / self.superview.frame.size.height)]];
[adjustAnchor setRemovedOnCompletion:NO];
[adjustAnchor setFillMode:kCAFillModeForwards];
//Create animation to rotate the container view within its superview.
CABasicAnimation *rotation = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
//Create the animation group to apply these transforms
CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
[animationGroup setAnimations:#[adjustAnchor,rotation]];
[animationGroup setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
[animationGroup setDuration:0.6];
//Apply the end results of the animations directly to the container views layer.
[self.superview.layer setTransform:CATransform3DRotate(CATransform3DIdentity, DEGREES_TO_RADIANS(-self.rotationInSuperview), 0.0f, 0.0f, 1.0f)];
[self.superview.layer setAnchorPoint:CGPointMake(self.center.x / self.superview.frame.size.width, self.center.y / self.superview.frame.size.height)];
[self.superview.layer addAnimation:animationGroup forKey:#"animationGroup"];
//Animate the alpha property of all ImpressLabels in the container view.
[self.superview bringSubviewToFront:self];
[UIView animateWithDuration:0.4 delay:0.0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
for (ImpressLabel *label in sender.view.superview.subviews) {
if ([label isKindOfClass:[ImpressLabel class]]) {
if (label != self) {
[label setAlpha:0.25f];
}else{
[label setAlpha:1.0f];
}
}
}
} completion:nil];
}
Now, to address some of the concerns listed in your question.
I've profiled this project in Instruments' allocations tool and it
only consumes about 3.2 MB overall, so I'd say this approach is
efficient enough.
The sample I've provided animates most objects in 2D space, with the exception of the scaling animation which is an illusion at best. What I've done here is meant to serve as an example of how this can be done and isn't a 100% complete demonstration because as I stated above, animation really isn't my area of expertise. However, from looking over the docs, it seems like the key to having a label rotated in the third dimension and adjusting its superview to rotate all the labels and leave the selected label flat would be the use of CATransform3DInvert(). Although I haven't yet had time to fully figure out it works, it looks like it might be exactly what is needed in this scenario.
As far as mirroring goes, I don't believe there will be any problems with properly scaling everything. Looking over Apple's Multiple Display Programming Guide, it looks like the object passed to the NSNotification UIScreenDidConnectNotification is a UIScreen object. Since this is the case, you can easily ask for this displays bounds, and adjust the frames of the labels and the container view accordingly.
Note: In this example, only 0, 90, 180, and -90 degree transforms animate 100% correctly, due to the anchor point's coordinates being incorrectly generated. It looks like the solution lies with CGPointApplyAffineTransform(<#CGPoint point#>, <#CGAffineTransform t#>), but again I haven't had as much time to play with it as I would have liked. Either way, this should be plenty to get you started with your reproduction.
This has definitely sparked my interest though and when I get a chance to work on this again, I'll gladly update this post with any new information. Hope this helps!

I think it would help if you draw the text on a CALayer. A library that could help with this is: https://github.com/ZaBlanc/CanvasKit
Draw the next step of your animation on a new CALayer and transform to that. You could chain the required animations using this library: https://github.com/yangmeyer/CPAnimationSequence or this library: https://github.com/clayallsopp/Walt

Related

CAEmitterLayer emit continuously in a line

I am creating a very simple particle system with CoreAnimation.
This is my cell:
UIImage *image = [UIImage imageNamed:#"spark"];
CAEmitterCell *cell = [CAEmitterCell emitterCell];
[cell setContents:(id)image.CGImage];
[cell setBirthRate:250.f];
[cell setScale:.25f];
[cell setColor:[UIColor colorWithRed:1.0 green:0.2 blue:0.1 alpha:0.5].CGColor];
[cell setLifetime:5.0f];
and layer:
CAEmitterLayer *emitterLayer = [CAEmitterLayer layer];
[emitterLayer setEmitterCells:#[cell]];
[emitterLayer setFrame:bounds];
[emitterLayer setRenderMode:kCAEmitterLayerAdditive];
[self.view.layer addSublayer:emitterLayer];
Now, I move the emitterLayer's position to wherever I'm touching:
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
self.emitterLayer.emitterPosition = [[touches anyObject] locationInView:self.view];
}
However, the problem is, it doesn't emit the particles continuously. But rather, at regular intervals(dotted as oppose to a line):
I thought perhaps it's because I'm not animating it. So I tried to add a simple animation to move the emitter from top left to bottom right of the screen just to see if that works:
CGPoint startPos = CGPointZero;
CGPoint endPos = CGPointMake(bounds.size.width, bounds.size.height);
CABasicAnimation* ba = [CABasicAnimation animationWithKeyPath:#"emitterPosition"];
ba.fromValue = [NSValue valueWithCGPoint:startPos];
ba.toValue = [NSValue valueWithCGPoint:endPos];
ba.duration = .5f;
[emitterLayer addAnimation:ba forKey:nil];
What I expected is a line be be formed with particles emitted continuously. However, what I see particles being emitted at intervals, like this:
Is it possible to have the emitterLayer follow your touch up and continuously emit these particles like a drawing and not be dotted?
Thanks
The effect you are looking to achieve (I saw a link in a comment) is probably best achieved without a particle system. What you usually use for this effect depends on individual approach, but I've had success with both.
One, is to create a render buffer: You draw directly onto this buffer with a colour at maximum value. This could be a UIView for instance. At a timed interval, you decrease every pixel value in this view by a percentage (say 0.05%). New pixels added start bright and fade over time this way, so you have no gaps between points. This is CPU intensive!
The other is to create a 'trianglestrip': you will need to go into a lower level graphics API for this such as OpenGL. The idea is that you create a section of triangles (or quads) that form a 'ribbon' under your finger. Each new section that is added begins fading to 0% alpha and is then removed. There is an implementation of this technique in cocos2d which is called CCMotionStreak that is perfect for this. The benefit is that you supply a texture (as you would a particle system) so it keeps designers happy
I'd suggest removing the animation and to create the line effect via the CAEmitterCell's velocity and emissionLongitude properties:
UIImage *image = [UIImage imageNamed:#"spark"];
CAEmitterCell *cell = [CAEmitterCell emitterCell];
[cell setContents:(id)image.CGImage];
[cell setBirthRate:250.f];
[cell setScale:.25f];
[cell setColor:[UIColor colorWithRed:1.0 green:0.2 blue:0.1 alpha:0.5].CGColor];
[cell setLifetime:5.0f];
[cell setEmissionLongitude:M_PI_4]; //The emission angle
[cell setVelocity:500]; //adjust as needed, together with BirthRate
CAEmitterLayer *emitterLayer = [CAEmitterLayer layer];
[emitterLayer setEmitterCells:#[cell]];
[emitterLayer setFrame:bounds];
[emitterLayer setRenderMode:kCAEmitterLayerAdditive];
[self.view.layer addSublayer:emitterLayer];
That looks awfully like the pattern you'd see using an ease-in ease-out animation curve, which I think is the default for CAAnimations. The points are closer together at either end, suggesting it is slower at the beginning and end.
Change the timing function of the animation to linear and you should see the effect you want.
To get a continuous line of emitters the only thing I could get to work was to slow the animation down, which doesn't really give the right effect. It looks like moving the emitter position skips a few renders, I'm not sure why.
You also need to play with the scaling a bit and probably use a CAShapeLayer to draw the actual line you're after.
In fact, a better solution is probably to use a smaller emitter layer which you move along at the end of the line, with a CAShapeLayer which you animate the strokeEnd of to leave the drawing in there.
It seems to me like your birthRate is set way too low. Try changing it from 250.f to something like 1500.f and post the results.

How to draw a coin (a grey circle with black text centered on it) in iOS?

I understand that the question might sound too simple, but only after struggling for several hours with this simple thing am I posting this here. (Yup, I am new to custom drawing and iOS dev in general).
I am trying to draw a coin. It is a gray circle with some text centered on it.
What I have tried so far:
CGFloat *radius = 30;
CGPoint center = self.view.center;
CGRect someFrame = CGRectMake(center.x - radius, center.y - radius,
2 * radius, 2 * radius);
UIView *circularView = [[UIView alloc] initWithFrame:someFrame];
circularView.backgroundColor = [UIColor grayColor];
UILabel *label = [[UILabel alloc] initWithFrame:someFrame];
label.text = #"5";
label.textColor = [UIColor blackColor];
label.textAlignment = NSTextAlignmentCenter;
[circularView addSubview:label];
circularView.clipsToBounds = YES;
circularView.layer.cornerRadius = radius;
[self.view addSubview:circularView];
Have tried this and other variants. But none of it is working. The label is not appearing, and the view is not being drawn in the center of the view as expected in landscape mode.
Is there any better way to do this, using CALayer or Quartz or Core Graphics? As I said, I am new to this.
First things first:
CGFloat *radius = 30;
That...shouldn't really even compile. Or at least there should be one mean warning. That's not what you want. You want to say this:
CGFloat radius = 30;
That asterisk is going to, just, that's bad. You'll know when you want to use a pointer to a primitive value and this just ain't one o' those times.
With that outta the way, you're on the right track, but it looks like you have a misunderstanding about coordinate spaces.
You initialize the circle with the frame someFrame. This is a frame that makes sense in the coordinate space of self.view, which is good, because that's where you're adding it (mostly, see side note below).
But then you set the label's frame to the same thing. Which might be okay, except that you're adding it to the circularView -- not to self.view. Which means that the frame that made sense as a frame for a child of self.view suddenly doesn't make very much sense at all. What you really want is just the following:
// The label will take up the whole bounds of the circle view.
// Labels automatically center their text vertically.
UILabel *label = [[UILabel alloc] initWithFrame:circularView.bounds];
// Center the text in the label horizontally.
label.textAlignment = NSTextAlignmentCenter;
// Make it so that the label's frame is tied to the bounds of
// the circular view, so that if its size changes in the future
// the label will still look right.
label.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
The problem that you have now is that label is way off somewhere else, sticking off the edge of circularView (and thus getting clipped) because someFrame doesn't make sense in circularView's coordinate space.
Side note:
This won't work well if self.view.frame.origin isn't CGPointZero. It usually is, but what you really want is the center of the view's bounds, not the center of the view's frame. self.view.center gives you a point in the coordinate space of self.view.superview. It just so happens that this will appear to work as long as self.view.frame.origin is {0, 0}, but to be more technically correct, you should say:
CGPoint center = CGPointMake(CGRectGetMidX(self.view.bounds),
CGRectGetMidY(self.view.bounds));
You can also make circularView remain in the center of the view even as the view's bounds change (for example, if you rotate the device), with the following:
circularView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin |
UIViewAutoresizingFlexibleRightMargin |
UIViewAutoresizingFlexibleTopMargin |
UIViewAutoresizingFlexibleBottomMargin;
Yes, typing out autoresizingMasks manually is the worst thing ever, but it's worth it in the long run (actually, in the long run, you'll probably go crazy and make a shorter helper method like me...).
You need to change the origin of the label's frame. When you add the label to the background view, the origin of its frame is relative to its superview.
So it would be something like CGRectMake(center.x - radius, center.y - radius, 2 * radius, 2 * radius) for the background view, CGRectMake(0, 0, 2 * radius, 2 * radius) for the label.
Besides that, you can skip the background view and tint the UILabel's background color.
Your first problem is that you are creating a frame for the view container (circularView) with some non zero x and y. Then you are creating a label and giving it the same frame (with non zero x and y). Then you add the label to the container view. The label's x and y will be relative to the container view's x and y (offset, not centered). If you want the label and the container to share the same location on screen then change it to this:
UILabel *label = [[UILabel alloc] initWithFrame: circularView.bounds];
Another problem is with how simple you are making this you could do it all with the label (ignore the container view).
UILabel *label = [[UILabel alloc] initWithFrame:someFrame];
label.text = #"5";
label.textColor = [UIColor blackColor];
label.textAlignment = NSTextAlignmentCenter;
label.clipsToBounds = YES;
label.layer.cornerRadius = radius;
label.backgroundColor = [UIColor grayColor];
]self.view addSubview:label];

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.

Animating UILabel with CoreAnimation / QuartzCore in iOS App

I actually stuck on a problem with animating a UILabel in my iOS Application.
After 2 days of searching the web for code snippets, still no result.
Every sample I found was about how to animate UIImage, adding it as a subview to UIView by layer. Is there any good example about animating a UILabel?
I found a nice solution for a blinking animation by setting the alpha property, like this:
My function:
- (void)blinkAnimation:(NSString *)animationID finished:(BOOL)finished target:(UIView *)target
{
NSString *selectedSpeed = [[NSUserDefaults standardUserDefaults] stringForKey:#"EffectSpeed"];
float speedFloat = (1.00 - [selectedSpeed floatValue]);
[UIView beginAnimations:animationID context:target];
[UIView setAnimationDuration:speedFloat];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:#selector(blinkAnimation:finished:target:)];
if([target alpha] == 1.0f)
[target setAlpha:0.0f];
else
[target setAlpha:1.0f];
[UIView commitAnimations];
}
Call my function on the UILabel:
[self blinkAnimation:#"blinkAnimation" finished:YES target:labelView];
But how about a Pulse, or scaling animation?
Unfortunately font size is not an animatable property of NSView. In order to scale a UILabel, you'll need to use more advanced Core Animation techniques, using CAKeyframeAnimation:
Import the QuartzCore.framework into your project, and #import <QuartzCore/QuartzCore.h> in your code.
Create a new CAKeyframeAnimation object that you can add your key frames to.
Create a CATransform3D value defining the scaling operation (don't get confused by the 3D part--you use this object to do any transformations on a layer).
Make the transformation one of the keyframes in the animation by adding it to the CAKeyframeAnimation object using its setValues method.
Set a duration for the animation by calling its setDuration method
Finally, add the animation to the label's layer using [[yourLabelObject layer] addAnimation:yourCAKeyframeAnimationObject forKey:#"anyArbitraryString"]
The final code could look something like this:
// Create the keyframe animation object
CAKeyframeAnimation *scaleAnimation =
[CAKeyframeAnimation animationWithKeyPath:#"transform"];
// Set the animation's delegate to self so that we can add callbacks if we want
scaleAnimation.delegate = self;
// Create the transform; we'll scale x and y by 1.5, leaving z alone
// since this is a 2D animation.
CATransform3D transform = CATransform3DMakeScale(1.5, 1.5, 1); // Scale in x and y
// Add the keyframes. Note we have to start and end with CATransformIdentity,
// so that the label starts from and returns to its non-transformed state.
[scaleAnimation setValues:[NSArray arrayWithObjects:
[NSValue valueWithCATransform3D:CATransform3DIdentity],
[NSValue valueWithCATransform3D:transform],
[NSValue valueWithCATransform3D:CATransform3DIdentity],
nil]];
// set the duration of the animation
[scaleAnimation setDuration: .5];
// animate your label layer = rock and roll!
[[self.label layer] addAnimation:scaleAnimation forKey:#"scaleText"];
I'll leave the repeating "pulse" animation as an exercise for you: hint, it involves the animationDidStop method!
One other note--the full list of CALayer animatable properties (of which "transform" is one) can be found here. Happy tweening!

Resources