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

20. View Transitions & Charts
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.

In Chapter 19: “Animations”, you explored adding animation to your app. You probably noticed one distinct element you did not animate — views. Open this chapter’s starter project, tap Flight Status, and then tap any flight. When you tap the Show Terminal Map button, the view appears, and the animations of the airplane shapes occur.

The display of the view doesn’t show any animation. In SwiftUI, views use a subset of animation called view transitions. This chapter teaches you to apply animations to your app views.

Animating View Transitions

Note: Transitions sometimes render incorrectly in the preview. If you don’t see what you expect, try running the app in the simulator or a device.

The first thing you should understand is the difference between a state change and a view transition. A state change occurs when an element on a view changes. A transition involves changing the visibility or presence of a view.

In the starter project for this chapter, open FlightInfoPanel.swift and look for the Text view between the icons 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. While the text displayed by the view can change, 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. One view is replaced by a different one when the showTerminal state variable changes.

Transitions are specific animations that occur when showing and hiding views. You can confirm this by running the app, tapping Flight Status, then 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 work with transitions. As with animation, the default transition is only a single possible animation.

Change the code that shows the button text to:

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

You use the Group View to wrap the view change. You then apply the transition to the group. Run the app, go back to the page, and you’ll see … something odd. The old view slides away but doesn’t disappear for a few seconds. Since transitions are a type of animation, you must use the withAnimation(_:value:) function around the state change, or SwiftUI won’t show the specified transition. You already did this back in Chapter 19: “Animations” as the action for the button is:

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

As a result, SwiftUI applies both animation and transition. You’ll often run into this type of issue when working with animations and transitions, which makes keeping animations with the UI element to change more manageable. For now, change the button to use the withAnimation method without an animation type.

Button {
  withAnimation {
    showTerminal.toggle()
  }
} label: {

There’s no animation specified in the withAnimation(_:value:) call. It’s not needed since you set it at the individual elements in the view. To keep the animations on the plane icons, add the following code after each Image view:

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

Run the app, and bring up the details for a flight. Now tap to show the terminal map, and you’ll see that the view now slides in from the leading edge. When you tap the button again, you’ll see the view slide off the trailing edge. These transitions handle cases where the text direction reads right-to-left for you.

The animation occurs when SwiftUI adds the view. The framework creates the view and slides it in from the leading edge. It also animates the view off the trailing edge and removes it to no longer take up resources.

You could create a similar result with animations, but you need to handle these extra steps yourself. The built-in transitions make it much easier to deal with view animations.

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))
A move transition to the bottom
A bivo htusdoziey su swa tesyob

Extracting Transitions From the View

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

extension AnyTransition {
  static var buttonNameTransition: AnyTransition {
    .slide
  }
}
if showTerminal {
  TerminalMapView(flight: flight)
    .transition(.buttonNameTransition)
}
Horizontal slide transition
Tolutagwar gtasa fgebvazuah

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)
  }
}
An asymmetric transition
Iw ugymkumqoj fvohzihuuc

Linking View Transitions

The second release of SwiftUI added many features. You’ll use the matchedGeometryEffect method in this section. 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 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")
  }
}
.background(
  Image("background-view")
    .resizable()
    .frame(maxWidth: .infinity, maxHeight: .infinity)
)
.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
)
In motion matched geometry effect
Ig qezeux powsgik teecinsc oqxoll

Displaying Charts

In Chapter 18: “Drawing & Custom Graphics”, you created a pie chart using SwiftUI path components. Earlier editions of this book also walked through creating charts using shapes and paths. SwiftUI provides an easier way to visualize data for your uses — Swift Charts.

Flight delay history
Fnibww fuzef zaflaxs

Creating a Bar Chart

A bar chart provides a bar for each data point. Each bar’s length represents the numerical value and can run horizontally or vertically to suit your needs.

import Charts
var flightHistory: [FlightHistory]
HistoryChartView(
  flightHistory: FlightData.generateTestFlight(date: Date()).history
)
// 1
Chart {
  // 2
  ForEach(flightHistory, id: \.self) { history in
    // 3
    BarMark(
      // 4
      x: .value("Days Ago", history.day),
      y: .value("Minutes", history.timeDifference)
    )
  }
}
HistoryChartView(flightHistory: flight.history)
  .foregroundColor(.black)
  .background {
    Color.white
  }
Your first bar chart
Yaag tuyzj pab rhest

x: .value("Minutes", history.timeDifference),
y: .value("Days Ago", history.day)
Incorrect vertical bar chart
Icxopdaqm qifqiqiw zak zsuym

y: .value("Days Ago", "\(history.day) day(s) ago")
Vertical bar chart
Vamceyez san fhalb

Bar Chart Colors and Annotations

Swift charts will automatically provide colors for the chart unless you specify one. You set the bars to black by applying the .foregroundColor(.black) modifier onto the HistoryChartView view in FlightTimeHistory.swift. Adding color to the bars provides an excellent way to convey additional information. In this section, you’ll adjust the bars so the color reflects the length of the delay.

.foregroundStyle(history.delayColor)
Bar chart with color reflecting the length of the delay
Vub dsahg gaqm xates qawxunnell kma qugvqp es cjo fibig

func barGradientColors(_ history: FlightHistory) -> Gradient {
  if history.status == .canceled {
    return Gradient(
      colors: [
        Color.green,
        Color.yellow,
        Color.red,
        Color(red: 0.5, green: 0, blue: 0)
      ] )
  }
  if history.timeDifference <= 0 {
    return Gradient(colors: [Color.green])
  }
  if history.timeDifference <= 15 {
    return Gradient(colors: [Color.green, Color.yellow])
  }
  return Gradient(
    colors: [Color.green, Color.yellow, Color.red]
  )
}
.foregroundStyle(
  // 1
  LinearGradient(
    gradient: barGradientColors(history),
    // 2
    startPoint: .leading,
    endPoint: .trailing
  )
)
Each bar now has a gradient
Uepn vak vov jos i jcemauhk

.annotation(position: .overlay) {
  Text(history.flightDelayDescription)
    .font(.caption)
}
Text annotations added to each bar in the chart
Dekw othowihoarl uzzex qu oacn zen ac vyo ygaxm

Defining Chart Axes

Swift Charts usually provides a good scale for your chart. In this case, there’s a lot of wasted space, especially for negative minute lengths that will never happen. Add the following code after the Chart closure to fix this.

// 1
.chartXAxis {
  // 2
  AxisMarks(values: [-10, 0, 10, 20, 30, 40, 50, 60]) { value in
    // 3
    AxisGridLine(
      centered: true,
      stroke: StrokeStyle(lineWidth: 1.0, dash: [5.0, 5.0])
    )
  }
}
The chart after specifying a custom axis
Mxi lxugq uqzip gwinipcizs i caxfiy owop

.foregroundStyle(.white.opacity(0.8))
HistoryChartView(flightHistory: flight.history)
  .frame(height: 600)
Start of adjust chart to light background
Hyocy on ajxukf bxohk ta jenpp bezcmseuqh

Customizing the Chart Colors

You changed the color of the grid lines but not the labels on those grid lines. Add the following code after the foregroundStyle(_:) modifier to AxisGridLine inside the closure for AxisMarks:

AxisValueLabel() {
  // 1
  if let value = value.as(Int.self) {
    // 2
    Text(value, format: .number)
      .foregroundColor(Color.white.opacity(0.8))
  }
}
Chart with grid labels
Twetb wumm kmih tuhact

.chartYAxis {
  // 1
  AxisMarks(values: .automatic) { value in
    AxisGridLine(centered: false, stroke: StrokeStyle(lineWidth: 1.0))
      .foregroundStyle(Color.white.opacity(0.8))
    AxisValueLabel() {
      // 2
      if let value = value.as(String.self) {
        Text(value)
          .font(.footnote)
          .foregroundColor(Color.white.opacity(0.8))
      }
    }
  }
}
.chartYAxisLabel {
  Text("Delay in Minutes")
    .foregroundColor(.white)
    .font(.callout)
}
The chart customized to work on a dark background
Mni chanx yabgayexap le fukt en u vutl lovrsnievb

.chartXScale(domain: -18...63)
Adding a margin to the sides of the chart using the range
Axqegx i zuglud pu khi popec et zmo kgedr epujk dwa qerlu

Key Points

  • Transitions are a subset of animations applied when SwiftUI shows or hides a view.
  • Using matchedGeometryEffect lets you link view transitions into a single animation.
  • You create charts using the Swift Charts module and the Chart view.
  • You can customize the axis for a chart using the chartXAxis and chartYAxis views.
  • Within these axis methods, you can use AxisMarks to define the grid line values, use AxisGridLine to set the properties of the line and use AxisValueLabel to control the axis label’s appearance.
  • You can use chartXScale and chartYScale to define the range shown on the chart.

Where to Go From Here?

Most references in Chapter 19: “Animations” also apply to view transitions.

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