Writing Custom Animations on iOS | Part II

We build professional apps for Android, iOS and mobile web.
Have a look

Part II: More Complex CoreAnimation

Disclaimer: The following post assumes you have read, and completed the first part in this anthology. If you haven't, please read it now or download the source code.

CATransform3DRotate - Turnstile

fit right

Following on from this previous post regarding the use of some pretty nifty (yet still basic) CATransform3D effects, we'll be opening up that project, and playing around with some more complex animation blocks.

First cab off the rank is CATransform3DRotate, which allows you to rotate objects along their X, Y and Z axes. We'll be tweaking the depth animation from last time, and making it a (very sloooow) Turnstile.

private var rotation: CGFloat {  
  return ZPositions.Rotation.rawValue
}

private func degreesToRadians(degrees: CGFloat) -> CGFloat {  
        return ((CGFloat(M_PI) * degrees) / 180.0)
}

// ...

enum ZPositions: CGFloat {  
  // ...
    case Rotation = 90
}

We're going to declare a computed property. This is for the amount of rotation we'll use. The other static function is for converting our angle into radians, which is what CATransform3DRotate needs to do the thing. For those unfamiliar with Pi (in the code as M_PI), it's approximate value is 3.14 and the numbers after the decimal point apparently never stop. 😯

Navigate down to the addDepthDownTo animation functions and tweak them until they match the following code:

// 7
func addDepthDownToAnimation() -> CATransform3D {  
    let toViewRotationDirection: CGFloat = reverse ? rotation : -rotation
    let toViewZ: CGFloat = reverse ? -distance : distance

    var rotationAndPerspectiveTransform: CATransform3D = CATransform3DIdentity
    rotationAndPerspectiveTransform.m34 = 1.0 / 1000.0
    rotationAndPerspectiveTransform = CATransform3DRotate(rotationAndPerspectiveTransform, degreesToRadians(toViewRotationDirection), 0, 1, 0);
    rotationAndPerspectiveTransform = CATransform3DTranslate(rotationAndPerspectiveTransform, 0, 0, toViewZ)
    return rotationAndPerspectiveTransform
}

func addDepthDownFromAnimation() -> CATransform3D {  
    let fromViewRotationDirection: CGFloat = reverse ? -rotation : rotation
    let fromViewZ: CGFloat = reverse ? distance : -distance

    var rotationAndPerspectiveTransform: CATransform3D = CATransform3DIdentity
    rotationAndPerspectiveTransform.m34 = 1.0 / 1000.0
    rotationAndPerspectiveTransform = CATransform3DRotate(rotationAndPerspectiveTransform, degreesToRadians(fromViewRotationDirection), 0, 1, 0);
    rotationAndPerspectiveTransform = CATransform3DTranslate(rotationAndPerspectiveTransform, 0, 0, fromViewZ)
    return rotationAndPerspectiveTransform
}

You'll notice there's some foo in there (the ternary operator) to figure out if the animation is supposed to reverse or not, and if so, do the opposite animation. You'll also see that we use our degreesToRadians:degrees function to give us the radian value needed for accurate rotation, without needing to write out ((angle * M_PI) / 180.0) all the time. Nifty huh!

Challenge:

Have a look at degreesToRadians(toViewRotationDirection), 0, 1, 0. Can you figure out which of the axes we're rotating on?

Run the project, and you'll see that your animation has gone from a basic, single directional depth animation, to including some rotation. However you may notice a problem. UIView by default, has an "Anchor Point" on the cartesian plane of CGPoint(x: 0.5, y: 0.5) which is in the dead center of the screen. Instead of the animations being like a proper Turnstile, they intersect and rotate in an x shape.

To mediate this, paste in this function:

func setAnchorPoint(anchorPoint: CGPoint, view: UIView) {  
    var newPoint: CGPoint = CGPointMake(view.bounds.size.width * anchorPoint.x, view.bounds.size.height * anchorPoint.y)
    var oldPoint: CGPoint = CGPointMake(view.bounds.size.width * view.layer.anchorPoint.x, view.bounds.size.height * view.layer.anchorPoint.y)

    newPoint = CGPointApplyAffineTransform(newPoint, view.transform)
    oldPoint = CGPointApplyAffineTransform(oldPoint, view.transform)

    var position: CGPoint = view.layer.position

    position.x -= oldPoint.x
    position.x += newPoint.x

    position.y -= oldPoint.y
    position.y += newPoint.y

    view.translatesAutoresizingMaskIntoConstraints = true
    view.layer.anchorPoint = anchorPoint
    view.layer.position = position
}

And, replace the code at Step //1 with the following:

setAnchorPoint(CGPoint(x: 0.0, y: 0.5), view: toView)  
setAnchorPoint(CGPoint(x: 0.0, y: 0.5), view: fromView)  
toView.layer.transform = addDepthDownToAnimation()  

Re-run the app, and you'll notice that the view rotates from the left hand side.

Challenge:

Tweak the anchor point code to make the views rotate from the right-hand side of the screen. 🎉

CATransform3DRotate - Pendulum

Now we'll tweak the above animation code again to create a Pendulum. Replace the anchorPoint: lines with the following:

setAnchorPoint(CGPoint(x: 0.5, y: 0.0), view: toView)  
setAnchorPoint(CGPoint(x: 0.5, y: 0.0), view: fromView)  

Then replace the animation methods with these:

// 7
func addDepthDownToAnimation() -> CATransform3D {  
    let toViewRotationDirection: CGFloat = reverse ? -rotation : rotation
    let toViewZ: CGFloat = reverse ? -distance : distance

    var rotationAndPerspectiveTransform: CATransform3D = CATransform3DIdentity
    rotationAndPerspectiveTransform.m34 = 1.0 / 1000.0
    rotationAndPerspectiveTransform = CATransform3DRotate(rotationAndPerspectiveTransform, degreesToRadians(toViewRotationDirection), 1, 0, 0);
    rotationAndPerspectiveTransform = CATransform3DTranslate(rotationAndPerspectiveTransform, 0.0, 0.0, toViewZ)
    return rotationAndPerspectiveTransform
}

func addDepthDownFromAnimation() -> CATransform3D {  
    let fromViewRotationDirection: CGFloat = reverse ? rotation : -rotation
    let fromViewZ: CGFloat = reverse ? distance : -distance

    var rotationAndPerspectiveTransform: CATransform3D = CATransform3DIdentity
    rotationAndPerspectiveTransform.m34 = 1.0 / 1000.0
    rotationAndPerspectiveTransform = CATransform3DRotate(rotationAndPerspectiveTransform, degreesToRadians(fromViewRotationDirection), 1, 0, 0);
    rotationAndPerspectiveTransform = CATransform3DTranslate(rotationAndPerspectiveTransform, 0.0, 0.0, fromViewZ)
    return rotationAndPerspectiveTransform
}

You can go through the code if you want, and update the method signatures to better reflect their function (i.e. addPendulumTo and addPendulumFrom) but for example purposes it's not necessary.

Also, update the enum with the following values.

enum ZPositions: CGFloat {  
    case Spatial = 300
    case Distance = 30
    case Rotation = 20
}
CATransform3DScale

If instead you wish to scale down your layers instead of translating and rotating, you should know enough by now to replace occurrences of CATransform3DTranslate with CATransform3DScale. The only difference here is 0.0 is the same as CGRect(.zero) and 1.0 is the original size. You're not limited to those values either. You can even scale a view up to 5.0 times its original size!

Final Thoughts

If you Download The Source Code, the above examples can be found in CustomPresentation_Part2.swift. To use it in Xcode, either swap out the file with CustomPresentation.swift or modify it with the above example code. If you want the possibility of reverting from this code back to the code from Part I, download a new copy from Github or backup the old file before you edit it.

Happy Animating! 🎉