Chapters

Hide chapters

SwiftUI by Tutorials

Fourth Edition · iOS 15, macOS 12 · Swift 5.5 · Xcode 13.1

Before You Begin

Section 0: 4 chapters
Show chapters Hide chapters

19. Animations & View Transitions
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

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, it’s challenging to show animations with static images in a book. In some cases, you will see pictures that use red highlights to reflect the motion to expect for some parts of this chapter. You will need to work through this chapter using the preview, the simulator or a device for the best idea of how the animations are working. 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(action: {
  showTerminal.toggle()
}, label: {
  HStack(alignment: .center) {
    Text(
      showTerminal ?
      "Hide Terminal Map" :
      "Show Terminal Map"
    )
    Spacer()
    Image(systemName: "airplane.circle")
      .resizable()
      .frame(width: 30, height: 30)
      .padding(.trailing, 10)
      .rotationEffect(.degrees(showTerminal ? 90 : -90))
  }
})
.animation(.linear(duration: 1.0), value: showTerminal)
Animation on the image rotation
Egoricaoy ed pno ijire seqeqaat

.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
Yekuat ulojecuom

Image(systemName: "airplane.circle")
  .resizable()
  .frame(width: 30, height: 30)
  .padding(10)
  .rotationEffect(.degrees(showTerminal ? 90 : 270))
  .animation(.linear(duration: 1.0), value: showTerminal)
Spacer()
Two icons
Sdo ovigw

.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
Eeho ib eub

.animation(.easeInOut(duration: 1.0), value: showTerminal)
.animation(.easeOut(duration: 1.0), value: showTerminal)
Ease out
Iisi oid

Ease in
Oivu ik

timingCurve
lowamtCuwbo

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
Fadhinej fawvhi pewyolob nutoew

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)
.animation(nil, value: showTerminal)
Animation on only one state change
Omerozoez em afdz ene fcizo cqawjo

.animation(.linear(duration: 1), value: showTerminal)
Simultaneously animations
Labukpikeuotmz ebigeriitp

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 DelayBarChart.swift in the SearchFlights group. This view contains the bar chart of flight delays created in Chapter 18: Drawing & Custom Graphics. You’re going to add some animation to the bars when they appear. First, add a state variable to the struct below the flight property.

@State private var showBars = 0.0
func minuteLength(_ minutes: Int, proxy: GeometryProxy) -> CGFloat {
  let pointsPerMinute = proxy.size.width / minuteRange
  return CGFloat(abs(minutes)) * pointsPerMinute * showBars
}
.animation(
  .easeOut.delay(0.5),
  value: showBars
)
.onAppear {
  showBars = 1.0
}
func minuteOffset(_ minutes: Int, proxy: GeometryProxy) -> CGFloat {
  let pointsPerMinute = proxy.size.width / minuteRange
  let offset = minutes < 0 ? 15 + minutes * Int(showBars) : 15
  return CGFloat(offset) * pointsPerMinute
}

Cascading animations

The delay() method allows you to specify a time in seconds to pause before the animation occurs. You used it in the previous section so the view was fully displayed before the bars were animated.

@State private var showBars = false
func minuteLength(_ minutes: Int, proxy: GeometryProxy) -> CGFloat {
  let pointsPerMinute = proxy.size.width / minuteRange
  return CGFloat(abs(minutes)) * pointsPerMinute
}

func minuteOffset(_ minutes: Int, proxy: GeometryProxy) -> CGFloat {
  let pointsPerMinute = proxy.size.width / minuteRange
  let offset = minutes < 0 ? 15 + minutes : 15
  return CGFloat(offset) * pointsPerMinute
}
.frame(
  width: showBars ?
    minuteLength(history.timeDifference, proxy: proxy) :
    0
)
.offset(
  x: showBars ?
    minuteOffset(history.timeDifference, proxy: proxy) :
    minuteOffset(0, proxy: proxy)
)
.onAppear {
  showBars = true
}
.animation(
  .easeInOut.delay(Double(history.day) * 0.1),
  value: showBars
)

Extracting animations from the view

To this point, you’ve defined animations directly within the view. For exploring and learning, that works well. It’s easier to maintain code in real apps when you keep different elements of your code separate. Doing so also lets you reuse them. In DelayBarChart.swift, add the following code above the body structure:

func barAnimation(_ barNumber: Int) -> Animation {
  return .easeInOut.delay(Double(barNumber) * 0.1)
}
.animation(
  barAnimation(history.day),
  value: showBars
)

Animating paths

Run the app and tap on Flight Status and then tap on a flight. Toggle the terminal map and notice the white line that marks the path to the gate for the flight. Open FlightTerminalMap.swift in the FlightDetails group, and you’ll see the line is determined using a set of fixed points scaled to the size of the view. 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(Color.white, lineWidth: 3.0)

Making a state change

To animate this path, you need a state change on a property that SwiftUI knows how to animate. Animations function because of the Animatable protocol. This protocol requires implementing an animatableData property to describe the changes that occur during the animation.

@State private var showPath = false
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)
    }
  }
}
var walkingAnimation: Animation {
  .linear(duration: 3.0)
  .repeatForever(autoreverses: false)
}
WalkPath(points: gatePath(proxy))
  .trim(to: showPath ? 1.0 : 0.0)
  .stroke(Color.white, lineWidth: 3.0)
  .animation(walkingAnimation, value: showPath)
.onAppear {
  showPath = true
}
Animated path to terminal
Ihuyubut gaxn mi mexcesiw

Animating view transitions

Note: Transitions often render incorrectly in the preview. If you do not see what you expect, try running the app in the simulator or on a device.

Text(
  showTerminal ?
  "Hide Terminal Map" :
  "Show Terminal Map"
)
if showTerminal {
  Text("Hide Terminal Map")
} else {
  Text("Show Terminal Map")
}
Group {
  if showTerminal {
    Text("Hide Terminal Map")
  } else {
    Text("Show Terminal Map")
  }
}
.transition(.slide)
Button(action: {
  withAnimation(
    .spring(
      response: 0.55,
      dampingFraction: 0.45,
      blendDuration: 0
    )
  ) {
    showTerminal.toggle()
  }
}, label: {
Button(action: {
  withAnimation {
    showTerminal.toggle()
  }
}, label: {
.animation(
  .spring(
    response: 0.55,
    dampingFraction: 0.45,
    blendDuration: 0
  ),
  value: showTerminal
)

View transition types

The default transition type changes the opacity of the view when adding or removing it. The view goes from transparent to opaque on insertion and from opaque to transparent on removal. You can create a more customized version using the .opacity transition.

.transition(.move(edge: .bottom))

Extracting transitions from the view

You can extract your transitions from the view as you did with animations. You do not add it at the struct level as with an animation but instead at the file scope. At the top of FlightInfoPanel.swift add the following:

extension AnyTransition {
  static var buttonNameTransition: AnyTransition {
    .slide
  }
}
if showTerminal {
  FlightTerminalMap(flight: flight)
    .transition(.buttonNameTransition)
}
Slide transition
Kveve wbidfegeil

Async transitions

SwiftUI lets you specify different transitions when adding and removing a view. Change the static property to:

extension AnyTransition {
  static var buttonNameTransition: AnyTransition {
    let insertion = AnyTransition.move(edge: .trailing)
      .combined(with: .opacity)
    let removal = AnyTransition.scale(scale: 0.0)
      .combined(with: .opacity)
    return .asymmetric(insertion: insertion, removal: removal)
  }
}

Linking view transitions

The second release of SwiftUI added many features. The one you’ll use in this section is the matchedGeometryEffect method. It allows you to synchronize the animations of multiple views. Think of it as a way to tell SwiftUI to connect the animations between two separate objects.

@State var selectedAward: AwardInformation?
@Binding var selected: AwardInformation?
AwardCardView(award: award)
  .foregroundColor(.black)
  .aspectRatio(0.67, contentMode: .fit)
  .onTapGesture {
    selected = award
  }
AwardGrid(
  title: "Test",
  awards: AppEnvironment().awardList,
  selected: .constant(nil)
)
ZStack {
  // 1
  if let award = selectedAward {
    // 2
    AwardDetails(award: award)
      .background(Color.white)
      .shadow(radius: 5.0)
      .clipShape(RoundedRectangle(cornerRadius: 20.0))
      // 3
      .onTapGesture {
        selectedAward = nil
      }
      // 4
      .navigationTitle(award.title)
  } else {
    ScrollView {
      LazyVGrid(columns: awardColumns) {
        AwardGrid(
          title: "Awarded",
          awards: activeAwards,
          selected: $selectedAward
        )
        AwardGrid(
          title: "Not Awarded",
          awards: inactiveAwards,
          selected: $selectedAward
        )
      }
    }
    .navigationTitle("Your Awards")
  }
}
.onTapGesture {
  withAnimation {
    selectedAward = nil
  }
}
.onTapGesture {
  withAnimation {
    selected = award
  }
}
@Namespace var cardNamespace
.matchedGeometryEffect(
  id: award.hashValue,
  in: cardNamespace,
  anchor: .topLeading
)
AwardGrid(
  title: "Awarded",
  awards: activeAwards,
  selected: $selectedAward,
  namespace: cardNamespace
)
AwardGrid(
  title: "Not Awarded",
  awards: inactiveAwards,
  selected: $selectedAward,
  namespace: cardNamespace
)
var namespace: Namespace.ID
@Namespace static var namespace
AwardGrid(
  title: "Test",
  awards: AppEnvironment().awardList,
  selected: .constant(nil),
  namespace: namespace
)
.matchedGeometryEffect(
  id: award.hashValue,
  in: namespace,
  anchor: .topLeading
)

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()
  .foregroundColor(.white)
  .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
Obepomih aibpnevo bordac

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.
  • Using matchedGeometryEffect lets you link view transitions into a single animation.
  • You can create high-performance animations by combining TimelineView and Canvas.

Where to go from here?

You can read more about animations in Getting Started with SwiftUI Animations at https://www.raywenderlich.com/5815412-getting-started-with-swiftui-animations.

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