Chapters

Hide chapters

SwiftUI Animations by Tutorials

First Edition · iOS 16 · Swift 5.7 · Xcode 14

Section I: SwiftUI Animations by Tutorials

Section 1: 11 chapters
Show chapters Hide chapters

9. Combining Animations
Written by Bill Morefield

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

Most of this book’s animations deal with user interaction. In earlier chapters, you used animation to draw the user’s attention to the desired area in your app. These animations help guide the user while at the same time adding polish and improving the app’s visual appearance.

In this chapter, you’ll build an animation to act as a reward for the user when the steeping timer ends. This animation will show liquid pouring into the view’s background and filling it up.

Since this is a more complex animation, you’ll build it in two parts. First, you’ll add the animation that resembles a rising liquid within a container. You’ll then use SpriteKit’s particle system to add the pouring liquid that appears to fill the container.

Building a Background Animation

Open the starter project for this chapter. You’ll see the familiar Tea Brewing from previous chapters.

The start project contains a new group called PourAnimation, which includes the TimerComplete view shown when a steeping timer finishes. To start the new animation, create a SwiftUI view file named PourAnimationView.swift in the PourAnimation folder`.

You’ll use this view to contain the new animation’s views. As with other animations, starting with a simple version and then expanding upon it to create the final animation is the easiest. At the top of the generated struct, add the following new properties:

@State var shapeTop = 900.0
let fillColor = Color(red: 0.180, green: 0.533, blue: 0.78)

This code adds a state property you’ll use to control the animation. You also define a blue color you’ll use as the liquid’s color. Update the body of the view to:

// 1
Rectangle()
  // 2
  .fill(fillColor)
  // 3
  .offset(y: shapeTop)
  // 4
  .onAppear {
    withAnimation(.linear(duration: 6.0)) {
      shapeTop = 0.0
    }
  }

Here’s what the code does:

  1. You define a Rectangle shape that you’ll replace with a more complex Shape later.

  2. You fill the Rectangle with the blue color you defined earlier.

  3. This offsets the rectangle by the amount of shapeTop. By changing shapeTop, you can change the position of the top of the rectangle on the view.

  4. When the view appears, you use an explicit linear animation that takes six seconds to complete. SwiftUI will apply the animation when you change shapeTop to zero. The animation will then animate the movement of the Rectangle from the initial position to the top of the view.

You need to add this new view to the view that shows when the timer finishes. Open TimerComplete.swift. This view consists of a ZStack, which starts with a backgroundGradient. After the gradient and before the VStack, add the following code:

PourAnimationView()

Run the app and select any tea. Start the timer and wait for it to complete. Once the timer finishes, you’ll see the animation as the blue rectangle fills the view over six seconds, like a cup filling with liquid. Remember, you can adjust the timer length.

Simple filled view clipped at the bottom.
Simple filled view clipped at the bottom.

The clipped area at the bottom seems out of place. By default, SwiftUI keeps a view from entering the device’s safe area. To eliminate the bar at the bottom, you need to tell SwiftUI to allow the view to extend into that area.

In TimerComplete.swift, change the call to the view to:

PourAnimationView()
  .ignoresSafeArea(edges: [.bottom])

ignoresSafeArea(_:edges:) tells SwiftUI to allow the view to extend into part of that bottom part of the safe area.

Run the app, start a timer and let it complete. The Rectangle’s fill color now extends to the bottom of the screen.

Fill animation no longer clipped
Fill animation no longer clipped

Now that you’ve built the basics of the pouring animation, you’ll make the top of the rising liquid more realistic in the next section.

Making a Wave Animation

If you watch a liquid pouting into a cup, you’ll see the top of the liquid is anything but a smooth, flat surface. It makes a much more chaotic and complex flow.

struct WaveShape: Shape {
  func path(in rect: CGRect) -> Path {
    Path()
  }
}
WaveShape()
  .stroke(.black)
  .offset(y: 200)
7 0 77 611 189 495 -4
I rowi jyihj

// 1
Path { path in
  // 2
  for x in 0 ..< Int(rect.width) {
    // 3
    let angle = Double(x) / rect.width * 360
    // 4
    let y = sin(Angle(degrees: angle).radians) * 100
    // 5
    if x == 0 {
      path.move(to: .init(x: Double(x), y: -y))
    } else {
      path.addLine(to: .init(x: Double(x), y: -y))
    }
  }
}
Initial sine graph drawn in a view
Utexeal dasu syaqg kvehz ax e waat

Animating the Sine Wave

Add the following new property to the top of the Shape before path(in:):

var waveTop: Double = 0.0
// 5
if x == 0 {
  path.move(to: .init(
    x: Double(x),
    y: waveTop - y
  ))
} else {
  path.addLine(to: .init(
    x: Double(x),
    y: waveTop - y
  ))
}
path.addLine(to: .init(x: rect.width, y: rect.height))
path.addLine(to: .init(x: 0, y: rect.height))
path.closeSubpath()
WaveShape(waveTop: 200.0)
  .fill(.black)
WaveShape(waveTop: shapeTop)
  .fill(fillColor)
  .onAppear {
    withAnimation(.linear(duration: 6.0)) {
      shapeTop = 0.0
    }
  }
Wave movement not animating.
Raho yumewejn sid owenanigv.

var animatableData: Double {
  get { waveTop }
  set { waveTop = newValue }
}
Wave now animating the rise.
Vozu wod efidahixy qda fena.

Modifying the Filling View

You can change the shape of a sine wave by changing three properties: amplitude, wave length and phase.

2 8 68 zisekozvsj uwqrujuhi 913 832 434 -4
Ucapojtw ux a xapo.

var amplitude = 100.0
var wavelength = 1.0
var phase = 0.0
// 4
let y = sin(Angle(degrees: angle).radians) * amplitude
// 3
let angle = Double(x) / rect.width * wavelength * 360.0
// 3
let angle = Double(x) / rect.width * wavelength * 360.0 + phase
WaveShape(
  waveTop: shapeTop,
  amplitude: 15,
  wavelength: 4,
  phase: 90
)
Wave shape after using new parameters.
Novo nkobe axbov edohg duz yawonibuxl.

Animating Multiple Parts of the Wave

When you added waveTop to WaveShape, you needed to implement animatableData so SwiftUI could animate it. Therefore, you might expect to do the same for the three additional properties before you can animate them.

// 1
var animatableData: AnimatablePair<
  AnimatablePair<Double, Double>,
  AnimatablePair<Double, Double>
  > {
  get {
    // 2
    AnimatablePair(
      AnimatablePair(waveTop, amplitude),
      AnimatablePair(wavelength, phase)
    )
  }
  set {
    // 3
    waveTop = newValue.first.first
    amplitude = newValue.first.second
    wavelength = newValue.second.first
    phase = newValue.second.second
  }
}
ElafalixkuKuez Huisbo Puerli UkixofuwzePiic Vuuhsi Moolma watefawwwx lqase nuwiXoy izyzumisa wepehl dohalc sofhc sogwh verotp
Puihhok wfixosr zonuluej eh UkomaqufnaPoam xgfasx vi qviqaqleon.

var waveHeight: Double {
  min(shapeTop / 10.0, 20.0)
}
amplitude: waveHeight,
Wave height shrinking as it nears the top of the view.
Dodo xiejyq lykarjucl ab ic loedp swi wol in dhe beer.

@State var wavePhase = 90.0
phase: wavePhase
withAnimation(
  .easeInOut(duration: 0.5)
  .repeatForever()
) {
  wavePhase = -90.0
}
Wave animation shifting horizontally.
Jahi oxiruhues lfocxonm pilenirvammz.

Adding Multiple Waves

While your wave resembles rising water, you can enhance the effect by adding more waves offset from the current wave.

@State var wavePhase2 = 0.0
let waveColor2 = Color(red: 0.129, green: 0.345, blue: 0.659)
WaveShape(
  waveTop: shapeTop,
  amplitude: waveHeight * 1.2,
  wavelength: 5,
  phase: wavePhase2
)
.fill(waveColor2)
withAnimation(
  .easeInOut(duration: 0.3)
  .repeatForever()
) {
  wavePhase2 = 270.0
}
Second moving wave added to the view
Lexirc webiqv rivo oxlez yu xgi saix

Animation With Particles

The most efficient way to create a pour animation, the animation of a liquid acting under gravity, is to use a particle system. A particle system is a group of points that change under rules that affect their behavior and appearance. They work well to create effects such as smoke, rain, confetti and fireworks.

Creating a Particle Emitter

Under the PourAnimation folder, create a new SpriteKit Particle File. For Particle template, select Rain and click Next. Name it PourParticle. The preview will show the new particle file, which resembles a light rain:

Default rain particle emitter
Leruoyg hiov mersiqci upafyas

Final particle system
Bowam tojyimle mlggiz

Color picker showing particle color.
Xuxax debmet qquvaxn layhammo pigog.

Building a SceneKit Scene

First, you need a SwiftUI view that’ll display your SceneKit scene. Inside the PourAnimation folder, create a new SwiftUI view file named PourSceneView. At the top of the new file, add a second import:

import SpriteKit
class PouringLiquidScene: SKScene {
  static let shared = PouringLiquidScene()
}
let dropEmitter = SKEmitterNode(fileNamed: "PourParticle")
override func didMove(to view: SKView) {
  // 1
  self.backgroundColor = .clear
  // 2
  if let dropEmitter,
     !self.children.contains(dropEmitter){
    self.addChild(dropEmitter)
  }

  // 3
  dropEmitter?.position.x = 100
  dropEmitter?.position.y = self.frame.maxY
}
var pouringScene: SKScene {
  // 1
  let scene = PouringLiquidScene.shared
  // 2
  scene.size = UIScreen.main.bounds.size
  scene.scaleMode = .fill
  // 3
  return scene
}
SpriteView(
  scene: pouringScene,
  options: [.allowsTransparency]
)
Preview of particle emitter in SwiftUI view.
Wdaxauw ij fuxvefre ewubqor of FwetpOO jioq.

Finishing the Animation

Open PourAnimationView.swift and add the following state property after wavePhase2:

@State var showPour = true
if showPour {
  PourSceneView()
}
Timer complete with particle animation with timings wrong.
Yohul vugnmimi bosk xojwucpo osayokeul hogc zupuqls npuhg.

withAnimation(
  .linear(duration: 6.0)
  .delay(1)
) {
  shapeTop = 0.0
}
DispatchQueue.main.asyncAfter(deadline: .now() + 7.0) {
  showPour = false
}
Final Timer completed animation
Yuqav Caxeg taygwacus usufexeok

Key Points

  • You can use animations to draw the user’s attention to an element and add a nice visual to reinforce the user’s action.
  • You can combine multiple animations to produce a finished visual effect for complex animations.
  • The SwiftUI animation system is robust and capable, but you can leverage other Apple frameworks when creating animations. SwiftUI lets you efficiently use them in your SwiftUI project.
  • SceneKit includes a particle system that works well to produce smoke, rain, confetti and fire.

Where to Go From Here?

Chapter 6: Introduction to Custom Animations and Chapter 7: Complex Custom Animations of this book go into more detail on using the AnimatableData and AnimatablePair protocols.

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