Chapters

Hide chapters

SwiftUI by Tutorials

Fifth Edition · iOS 16, macOS 13 · Swift 5.8 · Xcode 14.2

Before You Begin

Section 0: 4 chapters
Show chapters Hide chapters

19. Animations
Written by Bill Morefield

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

The difference between a good app and a great app often comes from the little details. Using the correct animations at the right places can delight users and make your app stand out in the App Store.

Animations can make your app more fun and easy to use, and they can play a decisive role in drawing the user’s attention to certain areas.

Animation in SwiftUI is much simpler than animation in AppKit or UIKit. SwiftUI animations are higher-level abstractions that handle all the tedious work for you. If you have experience with animations on Apple platforms, a lot of this chapter will seem familiar. You’ll find it a lot less effort to produce animations in your app. You can combine or overlap animations and interrupt them without care. Much of the complexity of state management goes away as you let the framework deal with it. It frees you up to make great animations instead of handling edge cases and complexity.

In this chapter, you’ll work through the process of adding animations to a sample project. Time to get the screen moving!

Animating State Changes

First, open the starter project for this chapter. Build and run the project for this chapter. You’ll see an app that shows flight information for an airport. The first option displays the flight status board, which provides flyers with the time and the gate where the flight will leave or arrive.

Flight board
Flight board

Note: Unfortunately, showing animations with static images in a book is challenging. Sometimes, you will see pictures with arrows reflecting the expected motion. You will need to work through this chapter using the preview, the simulator or a device for the best idea of how the animations work. The preview makes tweaking animations easier, but sometimes animations won’t look quite right in the preview. Try running the app in the simulator or on a device if you don’t see the same thing in the preview described here.

Adding Animation

To start, open FlightInfoPanel.swift in the FlightDetails group and look for the following code:

if showTerminal {
  FlightTerminalMap(flight: flight)
}
Button {
  showTerminal.toggle()
} label: {
  HStack {
    Text(showTerminal ? "Hide Terminal Map" : "Show Terminal Map")
    Image(systemName: "airplane.circle")
      .imageScale(.large)
      .padding(10)
      .rotationEffect(.degrees(showTerminal ? 90 : -90))
  }
}
The two states of the terminal map
Dfu fpo crisoh ow bde sevcuqoy leb

.animation(.linear(duration: 1.0), value: showTerminal)
Animation on the image rotation
Ayutagaed us rno adefe meworoor

.rotationEffect(.degrees(showTerminal ? 90 : 270))

Animation Types

So far, you’ve worked with a single type of animation: the linear animation. This provides a linear change at a constant rate from the original state to the final state. If you graphed the change vertically against time horizontally, the transition would look like this:

Linear animation
Beyaes umisigeij

Image(systemName: "airplane.circle")
  .imageScale(.large)
  .padding(10)
  .rotationEffect(.degrees(showTerminal ? 90 : 270))
  .animation(.linear(duration: 1.0), value: showTerminal)
Spacer()
New icons added
Vog omijq uffid

.animation(.default.speed(0.33), value: showTerminal)

Eased Animations

Eased animations might be the most common in apps. They generally look more natural since something can’t instantaneously change speed in the real world. An eased animation applies an acceleration, a deceleration or both at the endpoints of the animation. The animation reflects the acceleration or deceleration of real-world movement.

Ease in out
Ouhu ez uix

.animation(.easeInOut(duration: 1.0), value: showTerminal)
.animation(.easeOut(duration: 1.0), value: showTerminal)
Ease out
Uexe eaf

Ease in
Eezu uf

timingCurve
puxawqQoxbi

Spring Animations

Eased animations always transition between the start and end states in a single direction. They also never pass either end state. The other SwiftUI animations category lets you add a bit of bounce at the end of the state change. The physical model for this type of animation gives it the name: a spring.

Why a Spring Makes a Proper Animation

Springs resist stretching and compression — the greater the spring’s stretch or compression, the more resistance the spring presents. Imagine a weight attached at one end of a spring. Attach the other end of the spring to a fixed point and let the spring drop vertically with the weight at the bottom. It will bounce several times before coming to a stop.

Dampened simple harmonic motion
Fergapol ruzbja zespeges pazeop

Creating Spring Animations

Change the animation for the second icon to:

.animation(
  .interpolatingSpring(
    mass: 1,
    stiffness: 100,
    damping: 10,
    initialVelocity: 0
  ),
  value: showTerminal
)
.animation(
  .spring(
    response: 0.55,
    dampingFraction: 0.45,
    blendDuration: 0
  ),
  value: showTerminal
)

Removing and Combining Animations

A common problem in the initial release of SwiftUI arose in that animations could sometimes occur where you didn’t want them. Adding the value parameter to the animation(_:value:) addresses much of this problem. There still may be times that you may want to apply no animation. You do this by passing a nil animation type to the animation(_:value:) method.

.scaleEffect(showTerminal ? 1.5 : 1.0)
Image scaling and rotating
Uleze fjehavx eqd gewazupx

.animation(nil, value: showTerminal)
Animation on only one state change
Onirayiul ik elsx axi vhayi tzulpa

.animation(.linear(duration: 1), value: showTerminal)
Simultaneously animations
Zozexwuzaoogtn icihufiecs

Animating From State Changes

To this point in the chapter, you’ve applied animations at the view element that changed. You can also apply the animation where the state change occurs. When doing so, the animation applies to all changes that occur because of the state change.

withAnimation(
  .spring(
    response: 0.55,
    dampingFraction: 0.45,
    blendDuration: 0
  )
) {
  showTerminal.toggle()
}

Animating Shapes

Open TerminalStoresView.swift in the FlightDetails group. This view displays the stores in a terminal that you created in Chapter 18: “Drawing & Custom Graphics”. In this section, you’ll add some animation to these shapes when they appear. First, add a state variable to the struct below the flight property.

@State private var showStores = 0.0
let xOffset =
  Double(index) * storeSpacing * direction * showStores + firstStoreOffset
.animation(.easeOut, value: showStores)
.onAppear {
  showStores = 1.0
}
The stores sliding into place
Gho tforad qbagesf ikda dxeki

Cascading Animations

The delay() method allows you to specify a time in seconds to pause before the animation occurs. You can also use it to allow animations to chain together and provide a sense of progress or motion.

@State private var showStores = false
let xOffset = Double(index) * storeSpacing * direction + firstStoreOffset
.offset(
  x: showStores ?
    xOffset :
    firstStoreOffset - direction * width,
  y: height * 0.4
)
.onAppear {
  showStores = true
}
.animation(.easeOut.delay(Double(index) * 0.3), value: showStores)
Animation causing stores to appear one at a time
Ewasitois xeubody pzawod ro immoeb ogi eg e casi

Extracting Animations From the View

To this point, you’ve defined animations directly within the view. For exploring and learning, that works well. Maintaining code in real apps is easier when you keep different sections of your code separate. In TerminalStoresView.swift, add the following code above the body structure:

func storeAnimation(_ storeNumber: Int) -> Animation {
  return .easeInOut.delay(Double(storeNumber) * 0.3)
}
.animation(storeAnimation(index), value: showStores)

Animating Paths

Open GatePathView.swift in the FlightDetails group, and you’ll see a view that draws a line determined using a set of fixed points scaled to the view’s size. The code below draws the path:

Path { path in
  // 1
  let walkingPath = gatePath(proxy)
  // 2
  guard walkingPath.count > 1 else { return }
  // 3
  path.addLines(walkingPath)
}
.stroke(lineWidth: 3.0)
GatePathView(flight: flight)
  .foregroundColor(.white)
The gate path drawn over the terminal map
Rva xolo yoyl qfily ined hru wewrofer fak

Making a Path State Change

To animate this path, you need a state change on a property SwiftUI knows how to animate. A SwiftUI Shape has a method trim(from:to:) that trims a shape to a fractional portion based on its representation as a path. For a shape implemented as a path, the method provides a quick way to draw only a portion of the path.

struct WalkPath: Shape {
  var points: [CGPoint]

  func path(in rect: CGRect) -> Path {
    return Path { path in
      guard points.count > 1 else { return }
      path.addLines(points)
    }
  }
}
@State private var showPath = false
var walkingAnimation: Animation {
  .linear(duration: 3.0)
  .repeatForever(autoreverses: false)
}
WalkPath(points: gatePath(proxy))
  .trim(to: showPath ? 1.0 : 0.0)
  .stroke(lineWidth: 3.0)
  .animation(walkingAnimation, value: showPath)
.onAppear {
  showPath = true
}
Animated path to terminal
Ewerisez zorg yi xohhuneh

Making Canvas Animations

In Chapter 18: “Drawing & Custom Graphics”, you learned about the Canvas view meant to provide better performance for a complex drawing, mainly when it uses dynamic data. When combined with the TimelineView you used in Chapter 15: “Advanced Lists”, it provides a platform to create your animated drawings. In this section, you’ll create a simple animation of an airplane for the app’s initial view.

private var startTime = Date()
private let animationLength = 5.0
TimelineView(.animation) { timelineContext in
}
Canvas { graphicContext, size in
  // 1
  let timePosition = (timelineContext.date.timeIntervalSince(startTime))
    .truncatingRemainder(dividingBy: animationLength)
  // 2
  let xPosition = timePosition / animationLength * size.width
  // 3
  graphicContext.draw(
    Text("*"),
    at: .init(x: xPosition, y: size.height / 2.0)
  )
} // Extension Point
WelcomeAnimation()
  .frame(height: 40)
  .padding()
symbols: {
  Image(systemName: "airplane")
    .resizable()
    .aspectRatio(1.0, contentMode: .fit)
    .frame(height: 40)
    .tag(0)
}
guard let planeSymbol = graphicContext.resolveSymbol(id: 0) else {
  return
}
graphicContext.draw(
  planeSymbol,
  at: .init(x: xPosition, y: size.height / 2.0)
)
Animated airplane canvas
Emexecel uixzxode qetdef

Key Points

  • Don’t use animations only for the sake of doing so. Have a purpose for each animation.
  • Keep animations between 0.25 and 1.0 seconds in length. Shorter animations are often not noticeable. Longer animations risk annoying your user wanting to get something done.
  • Keep animations consistent within an app and with platform usage.
  • Animations should be optional. Respect accessibility settings to reduce or eliminate application animations.
  • Make sure animations are smooth and flow from one state to another.
  • Animations can make a huge difference in an app if used wisely.
  • You can create high-performance animations by combining TimelineView and Canvas.

Where to Go From Here?

If you want to dive deep into creating and using animation in SwiftUI, the book SwiftUI Animations by Tutorials at https://www.kodeco.com/books/swiftui-animations-by-tutorials is dedicated to the topic.

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 reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now