BlogChamira

The Illusion of iOS animations

I was given a task to write a progress animation in iOS from scratch and for a moment thought “Oops animation? Me?”. Even though I have a lot of experience in developing iOS apps whenever some difficult Core Graphics or Animation task comes I almost always look for free libraries in cocoacontrols.com due to time and budget constraints. BTW, cocoacontrols.com is my go to site when I have to do something quick but not this time around, remember I have to write it from scratch.

First things first, to do an animation programmatically, it is a must to see the animation in slow-motion, it is much easier to understand the movements of elements if you can see the animation frame by frame. The initial animation was done in CSS/JavaScript for web (I heard it’s quite easy, couple of lines may be?). With the help of my teammate Torbjørn I was able to get screen record of the animation.

(This Screen cast is taken from meny.no only for demonstration purposes)

As you can see, start state is a half-stroke rotating circle, with an image in center, end state is full-stroke non-rotating circle with a different image in center. Easy isn’t it? Not really, prior to the end state half-stroke rotating circle must be full-stroke (while rotating) with some linear animation. The moment it completes the stroke drawing center image should be changed with another fade-in animation.

Tip: Always good to break it down on a piece of paper and see how it going to be in detail.

Desktop 600x352 The Illusion of iOS animations

 

Example project can be found here: Source Code.

Step 1 – Add center image

  • This is probably the easiest task of all. Calculate X & Y positions and add an UIImageView.

Step 2 – Decide what are the best iOS drawing objects (components) to use.

  • If you look at the sketch or the video carefully, you can see only the stroke line rotates, the center image or the UIView itself does not rotate. To achieve that, I was left with two options, either add a CAShapeLayer and rotate it or add another UIView on top and rotate it. I preferred to use CAShapeLayer basically as it is the base of all animations in iOS/OSX. Using CAShapeLayer would definitely give me advantages because it’s supposed to do animation. It’s not only my gut feeling to use CAShapeLayer. With that kind of decision in mind I did some research before hand. After all, at the end of the day you have to back your decisions don’t you?

Step 3 – Draw path for half-stroke circle, add path(UIBezierPath) to CAShapeLayer & add CAShapeLayer to UIView.layer

  • UIBezierPath provides comprehensive Objective-c wrapper for path drawings, otherwise you can always use CGPathRef(Mutable) to draw paths to CAShapeLayer. I always prefer to do things Objective-C way than using underlying C libraries. Once half-stroke UIBezierPath is created it’s just a one line of code to add the path the CAShapeLayer.
  • Finally, colour the drawn half-stroke circle. It’s important that setting stroke colour should be applied on CAShapeLayer not on UIBezierPath. You may ask why? The answer is in next step.

Tip: Use some different colours to see different components and layers of the drawings. Once everything is working properly you can change them into the design specs colours.

a2 600x200 The Illusion of iOS animations

 -(CAShapeLayer * ) circleShapeLayer {
if (_circleShapeLayer == nil) {

_circleShapeLayer = [CAShapeLayer layer];
_circleShapeLayer.frame = self.bounds;
_circleShapeLayer.masksToBounds = YES;
_circleShapeLayer.cornerRadius = self.layer.cornerRadius;
_circleShapeLayer.path = self.circlePath.CGPath;
[self.layer addSublayer: _circleShapeLayer];

}
return _circleShapeLayer;

}

//half-stroke path
(UIBezierPath * ) circlePath {

if (_circlePath == nil) {

CGRect rect = [self circleShapeRect];
CGFloat radius = CGRectGetHeight(rect) / 2.0;
CGPoint center = CGPointMake(CGRectGetWidth(self.frame) / 2, CGRectGetHeight(self.frame) / 2);
_circlePath = [UIBezierPath bezierPathWithArcCenter: center radius: radius startAngle: 0 endAngle: DEGREES_TO_RADIANS(180) clockwise: true];
_circlePath.lineWidth = 0.0;

}
return _circlePath;

}

(CGRect) circleShapeRect {

CGFloat paddingFromBorder = self.padding;
CGRect rect = self.frame;
rect.origin.x += paddingFromBorder;
rect.origin.y += paddingFromBorder;
rect.size.width -= (2 * paddingFromBorder);
rect.size.height -= (2 * paddingFromBorder);
return rect;

}


Step 4 – Add Rotating animation & Start rotating half-stroke circle.

  • CABasicAnimation class provides mechanism to animate properties such as origin, size, transform in CALayer. For our solution it is needed to look into transform property simply because it has the rotation property which is used to rotate a CALayer. Once CABasicAnimation properties (duration, toValue, duration, repeatCount etc.) are set, add CABasicAnimation to CAShapeLayer. Technically, CAShapeLayer is rotating and UIBezierPath is just an illusion which pretends to be rotating.

Tip: Until stop animation method is triggered animation should be looped again and again to get that to work set repeatCount property in CABasicAnimation to very high value for an instance CGFLOAT_MAX system defined value can be used.

a1 600x200 The Illusion of iOS animations

 


 -(void) startAnimatingWithCompletionBlock:(void(^)()) completionBlock { 
if (self.isAnimating) {

return;

}
self.isAnimating = YES;
[self unfillCircle];
CGFloat duration = 1.0;
CGFloat numberOfRoations = 1.0;
CGFloat toValue = M_PI * 2.0 * numberOfRoations * duration;
CABasicAnimation* rotationAnimation = [CABasicAnimation animationWithKeyPath:@»transform.rotation»];
rotationAnimation.toValue = @(toValue);
rotationAnimation.duration = duration;
rotationAnimation.cumulative = YES;
rotationAnimation.repeatCount = CGFLOAT_MAX;
[self.circleShapeLayer addAnimation:rotationAnimation forKey:@»rotationAnimation»];
completionBlock != nil ? completionBlock() : nil;

}

//unfill (Half stroke)
(void) unfillCircle {

self.circlePath = nil;
[self.circleShapeLayer setPath:self.circlePath.CGPath];
self.centerImageView.image = self.busyStateImage;
[self setNeedsDisplay];

}

 

Step 5 – Stroke full-circle, before it starts finishing animation.

  • Now, the CAShapeLayer is rotating itself constantly, when stop animation method is triggered, UIBezierPath must be stroke with full-circle while it rotates. Since this step is kind of tricky, it’s good to break it down bit more. These are the steps
  1. Add remaining half to the existing UIBezierPath
  2. Initiate another CABasicAnimation for keyPath ‘strokeEnd
  3. Set fromValue of CABasicAnimation to 0.5 (half-stroke circle)
  4. Set toValue of CABasicAnimation to 1.0 (full-stroke circle)
  5. Set the duration of CABasicAnimation to the value you want, lets say 1.0 second
  6. Add the animation to CAShapeLayer

Once these steps are done, the full-stroke animation should work nicely.

animation f 600x256 The Illusion of iOS animations


-(void) strokEndAnimation {
NSTimeInterval duration = (NSTimeInterval) kFillCircleAnimationDuration;
CABasicAnimation *endAnimation = [CABasicAnimation animationWithKeyPath:@»strokeEnd»];
endAnimation.fromValue = @0.5;
endAnimation.toValue = @1;
endAnimation.duration = duration;
[self.circleShapeLayer addAnimation:endAnimation forKey:@»endStrokeAnimation»];

}

 

Tip: To see the smoothness of the animation, set CABasicAnimation duration property to a relatively high value (5 seconds) so things can be seen slowly.

Step 6 – Change the center image.

  • As per the prototype, the center image is replaced the moment full-stroke is drawn. So, it’s important to know when Step 5 is completed. How do we do that? Some may suggest to have NSTimer scheduled trigger after animation duration of Step 5. But that’s not the way. iOS QuartzCore library has a magic mechanism called CATransaction. CATransaction has a completion block (call back) which will be triggered when animation is completed for current Core Animation Transaction. Please look at code example for the correct syntax.  When CATransaction completion block is called the center image can be replaced.
  • Replacing the center image has its own Fade-In animation. This can be achieved by adding CATranstion animation to UIImageView.layer by setting CATranstion.type to CATransitionFade.

a3 600x200 The Illusion of iOS animations

 // changing center image animation -(void) stopAnimation { 
//Center picture changing animation
CATransition *transition = [CATransition animation];
transition.duration = (NSTimeInterval) kCheckmarkFadeInAnimationDuration;
transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
transition.type = kCATransitionFade;
[self.centerImageView.layer addAnimation:transition forKey:nil];
[self.circleShapeLayer removeAnimationForKey:@»rotationAnimation»];

}


Step 7 – Ending animation.

  • The last step is to notify the completion of the entire animation to caller. As I said earlier in Step 6 CATransaction can be used to determine the completion of fade-in replacement of the center image.

a4 600x200 The Illusion of iOS animations

Once these seven steps are done, the basic animation should work without any problem. But there are few things you must take into consideration.

  1. The moment stop animation method is triggered, it starts to do completion of the animation i.e. full-stroke the UIBezierPath and replaces the center image with fade-in animation which actually takes some time to finish. What will happen if stop animation method is called while the completion of previous call is still in progress? This progress view would go nuts. Wouldn’t it? Therefore, it’s compulsory to have a Boolean to act as a lock.
  1. Prototype animation has outline shadow, drawing rounded shadow is tricky in iOS because the moment you set masksToBounds (UIView.layer) & clipsToBounds(UIView) properties to YES to get rounded effect the shadow is no longer visible. Thus, you have to do some faking by adding another UIView and drawing shadow path on fake UIView or something else. There is no one specific way to do these things, you can always get around these problems when you start playing with UIView.

Discussion

  1. dispatch_once – is a good mechanism not only to have singleton class but also to use as a lock to make sure that piece of code is executed once. It’s been used as a lock to control stop animation method. Have a look how the dispatch token is reset.
  1. KVO – one of the main features in iOS development is KVO (key-value observing). In this small project it’s been used to observe some dynamic properties such as lineColor, lineWidth Make sure you remove the observers when indicator is de-allocated.

Challenge

Only few properties such as lineColor, shadowColor, busyStateImage, completeStateImage, padding and lineWidth are exposed as writable public properties, it’s a good challenge to expose some more properties like rotationDirection, startingPoint of the half-stroke circle, startStrokePercentage, endAnimationTime, centerImageChangeAnimationTime etc.

A lot of iOS developers think doing animations in iOS is hard and challenging. I have to agree with it to some extend but I find doing animations in iOS is always fun. You need to have lot of patients and ability to breakdown different phases of the animation into small isolated components because animation is all about illusion. It takes lot of time to fine tune the animation with objects/paths movements and timing of the movements but I really enjoyed it, I’m sure you will too. So, challenge yourself, jump off the deep end and learn to swim [Carson Kressley].

Example project can be found here: Source Code.

Reference:

Publisert