Chapters

Hide chapters

iOS Animations by Tutorials

Seventh Edition · iOS 15 · Swift 5.5 · Xcode 13

Section IV: Layer Animations

Section 4: 9 chapters
Show chapters Hide chapters

15. Shapes & Masks
Written by Marin Todorov

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

This chapter marks a bit of a shift in this section of the book: not only are you going to start working with a different sample project, but you’ll work with multi-layer effects, create layer animations that appear to interact physically with each other and morph between shapes as the animation runs.

If that sounds like a lot to take in, just think back to the great-looking animations you created in previous chapters with a relatively small bit of code!

The shapes in this chapter will be handled by CAShapeLayer, which is a CALayer sub-class that lets you draw various shapes on the screen, from the very simple to the very complex:

Instead of taking in drawing instructions, you give a the CALayer a CGPath to draw on screen. This comes in handy since Core Graphics already defines a very extensive API of drawing instructions for building CGPath shapes.

If you’re more familiar with UIBezierPath, you can use that to define a shape and then use its cgPath property to get its Core Graphics representation. You will give that a try later in this very chapter.

After you create your desired shape you can set such properties on as the stroke color, fill color and stroke dash pattern.

Of course, by now you’re likely asking “…but can I animate these properties?” Yes, you can:

  • path: Morph the layer’s shape into a different shape.
  • fillColor: Change the fill tint of shape to a different color.
  • lineDashPhase: Create a marquee or “marching ants” effect around your shape.
  • lineWidth: Grow or shrink the size of the stroke line of your shape.

There are two more animatable properties that you can use when drawing shapes; you’ll learn about these in Chapter 17: “Stroke & Path Animations.”

The project for this chapter simulates the starting screen of a combat game that is searching for an online opponent. You’ll simulate some online communication and add animations to show the communication state.

By the end of this chapter the project will look much like the screen below:

This chapter is designed to show you how to animate the new properties discussed above in the context of a common project you’d work on in real life. This will require a bit of extra work, but I know you’ll enjoy the ride!

Finishing up the Avatar View

Open the starter project for this chapter, select Main.storyboard and take a look at the user interface you’ll be working with in this chapter:

The project setup is fairly straightforward: a single view controller to display a nice background image, some labels, a “Search Again” button, and two avatar images, one of which will be empty until the app “finds” an opponent.

The two avatars are each an instance of the class AvatarView. In this section of the chapter, you’ll quickly finish writing the class code while you learn how AvatarView works.

Open AvatarView.swift and have a look at didMoveToWindow(), where you’ll build up the following elements of the avatar view:

  • photoLayer: The avatar’s image layer.
  • circleLayer: A shape layer for drawing a circle.
  • maskLayer: Another shape layer for drawing a mask.
  • label: A label to show the player’s name.

You’ll layer these on top of each other to build the composite avatar view as follows:

The above components already exist in the project, but haven’t been added to the view — that’s your first task. Add the following code to didMoveToWindow():

photoLayer.mask = maskLayer

This simply masks the square image above with the circle-shaped mask in maskLayer.

Build and run your project to see how things look; you can also see the change right in the storyboard thanks to @IBDesignable:

Now add the border layer to the avatar view’s layer in didMoveToWindow():

layer.addSublayer(circleLayer)

This adds the circular-shaped layer to the avatar, which frames it in nicely:

Both the mask layer and the frame layer are instances of CAShapeLayer; you’ll make use of this fact when you animate them in the next section.

There’s one more piece to add — the player name label. Add the following code to didMoveToWindow():

addSubview(label)

This wraps up the avatar view like so:

Now you’re ready to add some animations!

Creating the Bounce-Off Animation

The first animation you’ll create will make it appear as if the two avatars are bouncing off each other while your project “searches” for an opponent.

searchForOpponent()
func searchForOpponent() {
  let avatarSize = myAvatar.frame.size
  let bounceXOffset: CGFloat = avatarSize.width / 1.9
  let morphSize = CGSize(
    width: avatarSize.width * 0.85,
    height: avatarSize.height * 1.1)
}
let rightBouncePoint = CGPoint(
  x: view.frame.size.width / 2.0 + bounceXOffset,
  y: myAvatar.center.y)

let leftBouncePoint = CGPoint(
  x: view.frame.size.width / 2.0 - bounceXOffset,
  y: myAvatar.center.y)
myAvatar.bounceOff(
  point: rightBouncePoint, 
  morphSize: morphSize)

opponentAvatar.bounceOff(
  point: leftBouncePoint, 
  morphSize: morphSize)
func bounceOff(point: CGPoint, morphSize: CGSize) {
  let originalCenter = center

  UIView.animate(
    withDuration: animationDuration, 
    delay: 0.0,
    usingSpringWithDamping: 0.8, 
    initialSpringVelocity: 0.0, 
    animations: {
      self.center = point
    }, 
    completion: { _ in
      //complete bounce to
    })
}
UIView.animate(
  withDuration: animationDuration, 
  delay: animationDuration, 
  usingSpringWithDamping: 0.7, 
  initialSpringVelocity: 1.0, 
  animations: {
    self.center = originalCenter
  }, 
  completion: { _ in
    delay(seconds: 0.1) {
      self.bounceOff(point: point, morphSize: morphSize)
    }
  })

Morphing Shapes

When the two avatars collide, they should squish a little in this perfectly-elastic collision. The view controller will pass in a morph size that makes the avatar image slightly taller and narrower for this effect:

let morphedFrame = (originalCenter.x > point.x) ?

  CGRect(
    x: 0.0, 
    y: bounds.height - morphSize.height, 
    width: morphSize.width, 
    height: morphSize.height) :

  CGRect(
    x: bounds.width - morphSize.width, 
    y: bounds.height - morphSize.height, 
    width: morphSize.width, 
    height: morphSize.height)

let morphAnimation = CABasicAnimation(keyPath: "path")
morphAnimation.duration = animationDuration
morphAnimation.toValue = UIBezierPath(ovalIn: morphedFrame).cgPath

morphAnimation.timingFunction = CAMediaTimingFunction(name: .easeOut)
circleLayer.add(morphAnimation, forKey: nil)

maskLayer.add(morphAnimation, forKey: nil)

Key points

  • You can draw dynamic shapes on screen by using the CAShapeLayer class and setting its stroke and fill colors and set setting the path of the shape.
  • You can animate the shape rendered by CAShapeLayer by animating its path property.
  • CAShapeLayer is a great way to clip the contents of another layer by cutting out and displaying on screen a circle, square, or a star of the underlaying content.

Challenges

The challenges in this chapter are optional, but I encourage you to work through them to practice your skills and add some real polish to your project. However if you’re eager to start with gradient animations, then you can head straight on to the next chapter.

Challenge 1: Finish the Communication State Animations

For this challenge, you get a bit of a breather as you can simply follow along with the instructions below. Your task in this challenge is to add some status messages to show to the user as your faux “searching for an opponent” task progresses.

delay(seconds: 4.0, completion: foundOpponent)
func foundOpponent() {
  status.text = "Connecting..."

  opponentAvatar.image = UIImage(named: "avatar-2")
  opponentAvatar.name = "Ray"
}

delay(seconds: 4.0, completion: connectedToOpponent)
func connectedToOpponent() {
  myAvatar.shouldTransitionToFinishedState = true
  opponentAvatar.shouldTransitionToFinishedState = true
}
delay(seconds: 1.0, completion: completed)
func completed() {
  status.text = "Ready to play"
  UIView.animate(withDuration: 0.2) {
    self.vs.alpha = 1.0
    self.searchAgain.alpha = 1.0
  }
}

Challenge 2: Morph the Avatars to Squares

At this point the avatars just keep bouncing forever. Once the game is connected to an opponent, you’d like to stop the animation and reflect the state change in the UI.

if self.shouldTransitionToFinishedState {
  self.animateToSquare()
}

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now