Using View Transitions

This lesson’s introduction described view transitions as a type of animation, but one applied when a view appears or disappears. A state change occurs when an element on a view changes and can trigger an animation. Changing a view’s visibility or presence can similarly trigger a view transition.

Note: Transitions are unreliable in the preview. If you don’t see what you expect, try running the app in the simulator or on a device.

Open FlightInfoPanel.swift and look for the Text view in the button that shows the terminal map.

Right now, it looks like this:

Text(showTerminal ? "Hide Terminal Map" : "Show Terminal Map")

This code shows a state change. Although the text displayed by the view changes when showTerminal changes, it remains in the same view. Change the code to the following:

if showTerminal {
  Text("Hide Terminal Map")
} else {
  Text("Show Terminal Map")
}

Now, you have a view transition. When showTerminal changes, the view is replaced by a different one.

Transitions are specific animations that occur when showing and hiding views. You can confirm this by running the app, tapping Flight Status, and tapping any flight. Tap the button to show and hide the terminal map a few times, and notice how the view disappears and reappears. By default, views transition on and off the screen by fading in and out.

Much of what you’ve already learned about animations applies to transitions. As with animation, the default transition is only one possibility.

Change the code that shows the button text to:

Group {
  if showTerminal {
    Text("Hide Terminal Map")
  } else {
    Text("Show Terminal Map")
  }
}
.transition(.scale)

A .scale() transition causes the view to expand when inserted from a single point or to collapse when removed to a single point at the center. You can optionally specify a scale factor parameter for the transition. The scale factor defines the ratio of the initial view’s size. A scale of 0 provides the default transition to a single point. A value less than 1 causes the view to expand from that scaled size when inserted or collapse to it when removed. Values greater than one work the same; however, the view at the end of the transition is larger than the final view.

You can also specify an anchor parameter for the point on the view where the transition centers. An enumeration provides constants for the view’s corners, sides, and center. You can also provide a custom offset.

You wrap the conditional for the views inside a Group view so you can apply the transition to the group. This keeps you from duplicating the transition for each Text view.

Because transitions are a type of animation, you must use the withAnimation(_:value:) function around the state change. You already did this in the last section with the button:

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

Run the app, go back to the page, and you see the disappearing view shrink away as the new view expands into place. You see the transition applied to the text and the spring animations on the images both process with no problems. The animation on the images occurs as SwiftUI adds the view. The framework creates the view and expands it from the center as it shrinks the existing view down to a point. It also animates the view off the trailing edge and removes it so it no longer uses resources.

To separate the spring animations from the view transitions, change the action of the button to:

withAnimation {
  showTerminal.toggle()
}

Now, apply the spring animation directly after each .rotationEffect(.degrees(showTerminal ? 90 : 270)):

.animation(
  .spring(
    response: 0.55,
    dampingFraction: 0.45,
    blendDuration: 0
  ),
  value: showTerminal
)

Now, your transitions use the default animation, whereas the images still use the spring animation.

View Transition Types

The default transition type changes the view’s opacity 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.

The .move(edge:) transition moves the view from and to a specified edge of .top, bottom, .leading, or .trailing. The slide transition is a move transition predefined to insert a view from the leading edge and to remove it off the trailing edge. Both come with one important consideration. Add the following code after TerminalMapView(flight: flight):

.transition(.slide)

Run the app and bring up the information for any flight. Show the terminal map, and you see it slide in from the leading edge. When you tap Hide Terminal Map, the map view slides toward the trailing edge. But then it stops and remains visible for about one second before vanishing. This occurs because SwiftUI move transitions apply in the containing view and not the screen. Be careful to only use these transitions where the result won’t leave such an artifact.

View not sliding fully off the screen
View not sliding fully off the screen

To hide the lingering view, change the transition for the TerminalMapView(flight: flight) to:

.transition(.slide.combined(with: .opacity))

The combined(with:) method lets you combine two transitions when SwiftUI adds or removes a view. The two transitions occur simultaneously. In this case, the added view slides in from the leading edge as it fades in. The removed view slides toward the trailing edge and fades out. This fade-out means the lingering view on the screen won’t be visible. This situation is also common enough to have a shortcut in the .push transition, which animates the view’s insertion by moving it in from the specified edge while fading in. The view’s removal animates by moving it out toward the opposite edge and fading it out. Change the transition to:

.transition(.push(from: .leading))

The finished push transition
The finished push transition

Extracting Transitions From the View

You can extract your transitions to centralize them and simplify the view. At the file scope — meaning before the start of the struct — add the following code:

extension AnyTransition {
  static var terminalMapTransition: AnyTransition {
    .slide.combined(with: .opacity)
  }
}

This extension declares your transition as a static property of AnyTransition. Now, use it on the TerminalMapView. Change the transition modifier after TerminalMapView(flight: flight) to:

.transition(.terminalMapTransition)

You can run the app, tap Flight Status, tap any flight, and then tap Show Terminal Map to confirm nothing changed.

Horizontal slide transition
Horizontal slide transition

This becomes more useful with complex transitions such as specifying different transitions when adding and removing a view. Change the static property to:

extension AnyTransition {
  static var terminalMapTransition: 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)
  }
}

You see the view moves in from the trailing edge as it fades in. When SwiftUI removes the view, it shrinks to a point while fading out.

An asymmetric transition
An asymmetric transition

Now that you’ve learned about animation and transitions, you’ll next see how to link transitions into more complex animations.

See forum comments
Download course materials from Github
Previous: Multiple Animations & Animating Paths Next: Linking View Transitions