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

6. Intro to Custom 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

In this book, you’ve explored many ways SwiftUI makes animation simple to achieve. By taking advantage of the framework, you created complex animations with much less effort than previous app frameworks required. For many animations, this built-in system will do everything that you need. However, as you attempt more complex animations, you’ll find places where SwiftUI can’t do what you want without further assistance.

Fortunately, the animation support in SwiftUI includes protocols and extensions that you can use to produce animation effects beyond the basics while still having SwiftUI handle some of the work. This support lets you create more complex animations while still leveraging SwiftUI’s built-in animation capabilities.

In this chapter, you’ll start by adding a standard SwiftUI animation to an app. Then you’ll learn to implement animations beyond the built-in support while having SwiftUI handle as much as possible.

Animating the Timer

Open the starter project for this chapter. You’ll find an app that helps users brew and prepare tea. Build and run the app and tap any of the provided teas.

The app lists several types of tea and provides suggestions on water temperature and the amount of tea leaves needed for the desired amount of water. It also provides a timer that counts down the time needed to steep the tea.

You’ll see information for brewing the tea you selected. Adjust the amount of water, and it’ll update the needed amount of tea leaves to accommodate the change. It also lets you start and stop a timer for steeping the tea. When you start the brewing timer, it begins a countdown until your steeping completes.

Once it finishes, a view tells you your tea is ready to drink.

While it works, the app lacks energy and excitement. You’ll add some animations that give it more energy and help users in their quest for the best tea.

First, the ring around the timer turns blue when you start the timer. While the color change does show the timer is running, it doesn’t attract the eye. To do so, you’ll animate the timer’s border as a better indicator of a running timer.

Open TimerView.swift, and you’ll see the code for this view. The CountingTimerView used in this view contains the control for the timer. It currently uses overlay(alignment:content:) to add a rounded rectangle with the color provided by the timerBorderColor computed property. You’ll add a special case to display an animated border when the timer is running.

After the existing state properties, add the following new property:

@State var animateTimer = false

You’ll use this property to control and trigger the animation by toggling its value. The animation here will animate the border around the timer display and controls. You’ll animate the border so it appears to move in a circle around the digits and controls. To do this, you’ll create an angular gradient or conic gradient.

Unlike the more common linear gradient, which blends colors based on the distance from a starting point, an angular gradient blends colors as it sweeps around a central point. Instead of the distance from the starting point determining the color, the angle from the central point determines the color. All points along a line radiating from the center will share the same color.

Add the following code after the existing computed properties to create the angular gradient:

var animationGradient: AngularGradient {
  AngularGradient(
    colors: [
      Color("BlackRussian"), Color("DarkOliveGreen"), Color("OliveGreen"),
      Color("DarkOliveGreen"), Color("BlackRussian")
    ],
    center: .center,
    angle: .degrees(animateTimer ? 360 : 0)
  )
}

You specify the gradient will begin as a dark shade of black, transition to olive green at the midpoint, and then back to the same shade of black at the end. You set the gradient to use the center of the view as its origin. To allow animation, you set the angle by multiplying animateTimer by 360 degrees.

Toggling animaterTimer to true will rotate the gradient in a complete revolution. Note that the gradient will transition through a complete circle since you only specify a single angle. SwiftUI positions the start of the gradient at that angle and sweeps through the full rotation to the final color. It’ll provide a circular shift from nearly black through olive green around the circle and then back to nearly black, where the gradient started.

Now find the overlay modifier on CountingTimerView and replace its contents with:

switch timerManager.status {
  case .running:
    RoundedRectangle(cornerRadius: 20)
      .stroke(animationGradient, lineWidth: 10)
  default:
    RoundedRectangle(cornerRadius: 20)
      .stroke(timerBorderColor, lineWidth: 5)
}

While the timer runs, you apply a different style to stroke(_:lineWidth:) that uses the gradient you just added. You also widen the line to draw the eye and provide more space for the animation to show, and add another visual indicator that something has changed.

Now, build and run the app. Tap any tea and then start the timer. The border takes on the new broader gradient but doesn’t animate yet. You’ll do that in the next section.

Animating the Gradient

Still in TimerView.swift, find onChange(of:perform:) on NavigationStack. This modifier monitors changes to the timer’s status. Currently, it only checks for the .done state on the timer. Add a new case to the existing switch statement:

case .running:
  // 1
  withAnimation(
    .linear(duration: 1.0)
    // 2
    .repeatForever(autoreverses: false)
  ) {
    // 3
    animateTimer = true
  }

Animating the Pause State

You’ll also add an animation when the user pauses the time. First, add the following state property after the existing ones:

@State var animatePause = false
case .paused:
  RoundedRectangle(cornerRadius: 20)
    .stroke(.blue, lineWidth: 10)
    .opacity(animatePause ? 0.2 : 1.0)
animatePause = false
case .paused:
  // 1
  animateTimer = false
  // 2
  withAnimation(
    .easeInOut(duration: 0.5)
    .repeatForever()
  ) {
    animatePause = true
  }
// 3
animateTimer = false
animatePause = false

Making a View Animatable

As mentioned in Chapter 1: Introducing SwiftUI Animations, an animation is a series of static images changing rapidly and providing the illusion of motion. When SwiftUI animates a shape, it rapidly draws the view many times. The type of animation and the elements that change determine how the views change. In the previous section, you changed the angle of the angular gradient and SwiftUI animated the result of that change.

struct NumberTransitionView: View, Animatable {
var number: Int
var suffix: String

var animatableData: Double {
  get { Double(number) }
  set { number = Int(newValue) }
}
Text(String(number) + suffix)
NumberTransitionView(number: 5, suffix: " °F")

Using an Animatable View

Open BrewInfoView.swift. You’ll add a bit of animation to the brewing temperature that appears on the view. Add the following new property after the existing state properties:

@State var brewingTemp = 0
.onAppear {
  withAnimation(.easeOut(duration: 0.5)) {
    brewingTemp = brewTimer.temperature
  }
}
Text("\(brewingTemp) °F")

NumberTransitionView(number: brewingTemp, suffix: " °F")

Creating a Sliding Number Animation

Open CountingTimerView.swift. On the first line of the VStack, you’ll see the timer currently displays a string from timerManager. This string shows the remaining time formatted using a DateComponentsFormatter that shows only the minutes and seconds. The result provides the information, but it’s a bit plain.

var digits: [Int]
var hasMinutes: Bool {
  digits[0] != 0 || digits[1] != 0
}
HStack {
  // 1
  if hasMinutes {
    // 2
    if digits[0] != 0 {
      Text(String(digits[0]))
    }
    // 3
    Text(String(digits[1]))
    Text("m")
  }
  // 4
  if hasMinutes || digits[2] != 0 {
    Text(String(digits[2]))
  }
  // 5
  Text(String(digits[3]))
  Text("s")
}
TimerDigitsView(digits: [1, 0, 0, 4])
TimerDigitsView(digits: timerManager.digits)

struct SlidingNumber: View, Animatable {
var number: Double

var animatableData: Double {
  get { number }
  set { number = newValue }
}
SlidingNumber(number: 0)

Building an Animation

When developing an animation, it helps to consider the visual effect you want to achieve. Go back to the inspiration of a sliding scale of digits. You’ll implement a strip of numbers starting at nine and then moving down through zero. When the digit changes, the strip of numbers shifts to show the new value.

Implementing Sliding Numbers

First, you need a vertical strip of numbers. Delete the existing Text view inside the body, and add the following code at the top of the view body:

// 1
let digitArray = [number + 1, number, number - 1]
  // 2
  .map { Int($0).between(0, and: 10) }
let shift = number.truncatingRemainder(dividingBy: 1)
// 1
VStack {
  Text(String(digitArray[0]))
  Text(String(digitArray[1]))
  Text(String(digitArray[2]))
}
// 2
.font(.largeTitle)
.fontWeight(.heavy)
// 3
.frame(width: 30, height: 40)
// 4
.offset(y: 40 * shift)
HStack {
  if hasMinutes {
    if digits[0] != 0 {
      SlidingNumber(number: Double(digits[0]))
    }
    SlidingNumber(number: Double(digits[1]))
    Text("m")
  }
  if hasMinutes || digits[2] != 0 {
    SlidingNumber(number: Double(digits[2]))
  }
  SlidingNumber(number: Double(digits[3]))
  Text("s")
}

// 1
.overlay {
  RoundedRectangle(cornerRadius: 5)
    .stroke(lineWidth: 1)
}
// 2
.clipShape(
  RoundedRectangle(cornerRadius: 5)
)

Challenge

Using what you’ve learned in this chapter, adjust the timer animation so the digits slide in the opposite direction and the numbers slide downward. As a hint, recall that in SwiftUI, a decrease in offset will cause a shift upward. How can you make that move down instead?

Key Points

  • An angular gradient shifts through colors based on the angles around a central point.
  • The Animatable protocol provides a method to help you handle the animation within a view yourself. You only need to turn to it when SwiftUI can’t do things for you.
  • When using the Animatable protocol, SwiftUI will provide the changing value to your view through the animatableData property.
  • When creating custom animations using the Animatable protocol, begin by visualizing what you want the finished animation to look like.
  • Take advantage of SwiftUI’s ability to combine elements. In many cases, breaking an animation into smaller components will make it easier. You’ll find it easier to animate individual digits instead of trying to animate an entire display of numbers.

Where to Go From Here?

  • You can read more about angular gradients in Apple’s Documentation.
  • You can find other examples of using the Animatable protocol in Getting Started with SwiftUI Animations.
  • You’ll also explore the Animatable protocol more in the next section, including learning how to deal with animations involving multiple elements.
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