Building Engaging User Interfaces with SwiftUI

Mar 12 2025 · Swift 5.9, iOS 17.0, XCode 15.0

Lesson 02: Implementing Complex UI Layouts

Showing Flight Progress

Episode complete

Play next episode

Next
Transcript

Next, you’ll add an indicator of a flight’s progress to the card. The status of a flight will usually be either before departure or after landing. In between, there’s a time when the flight will be partway between airports.

Add the following code to the FlightCardView after the flight property:

func minutesBetween(_ start: Date, and end: Date) -> Int {
  // 1
  let diff = Calendar.current.dateComponents(
    [.minute], from: start, to: end
  )
  // 2
  guard let minute = diff.minute else {
    return 0
  }
  // 3
  return abs(minute)
}

This method takes two Date objects and returns the minutes between them.

  1. The dateComponents(_:from:to:) method returns the difference between two dates in the requested units — in this case, minutes.
  2. If something went horribly wrong and the minute property doesn’t exist, return zero minutes.
  3. Return the absolute value of the number of minutes. The absolute value returns only the magnitude, ignoring the sign, always resulting in a positive value.

You can use this method to get the flight’s progress as Double between zero and one. Add the following new method after minutesBetween(_:and:):

func flightTimeFraction(flight: FlightInformation) -> Double {
  // 1
  let now = Date()
  // 2
  if flight.direction == .departure {
    // 3
    if flight.localTime > now {
      return 0.0
    // 4
    } else if flight.otherEndTime < now {
      return 1.0
    } else {
      // 5
      let timeInFlight = minutesBetween(
        flight.localTime, and: now
      )
      // 6
      let fraction =
        Double(timeInFlight) / Double(flight.flightTime)
      return fraction
    }
  } else {
    if flight.otherEndTime > now {
      return 0.0
    } else if flight.localTime < now {
      return 1.0
    } else {
      let timeInFlight = minutesBetween(
        flight.otherEndTime, and: now
      )
      let fraction =
        Double(timeInFlight) / Double(flight.flightTime)
      return fraction
    }
  }
}

There’s a lot here, and it’s somewhat repetitive, but that’s necessary to handle all the possible cases:

  1. You put the current Date into a variable, which you’ll refer to often in this method.
  2. The first case covers departing flights. The case for arriving flights works the same but with the times swapped.
  3. If the localTime for the departing flight is after now, the flight has not departed yet, meaning the fraction is zero.
  4. If the otherEndTime parameter for the departing flight is before now, the flight already arrived, meaning the fraction is one.
  5. If neither is true, the flight is in the air. This code uses the minutesBetween(_:and:) method to get the minutes between now and the flight’s departure time in minutes.
  6. The flightTime parameter stores the flight’s total length. You calculate the fraction as the value calculated in the last step, divided by the flight’s length.

With a method to calculate the flight’s location, you’ll add a graphical representation in the next section.

Adding Inline Drawings

Now, you’ll add a view to show the flight’s progress. Create a new SwiftUI view named FlightProgressView inside the Timeline group. Change the view to:

struct FlightProgressView: View {
  var flight: FlightInformation
  var progress: CGFloat

  var body: some View {
    // 1
    GeometryReader { proxy in
      Image(systemName: "airplane")
        .resizable()
        // 2
        .offset(x: proxy.size.width * progress)
        .frame(width: 25, height: 25)
        .foregroundColor(flight.statusColor)
      // 3
    }.padding([.trailing], 20)
  }
}

#Preview {
  FlightProgressView(
    flight: FlightData.generateTestFlight(date: Date()),
    progress: 0.67
  )
}

This code uses a GeometryReader to allow the fine positioning of an image.

  1. The GeometryReader causes the view to fill the space. It also provides a GeometryProxy, which contains the view’s width, among other information.
  2. You take the view’s width from the size property on the GeometryProxy. Multiplying this value by the fraction of the flight gives an offset to reflect the flight’s progress as a portion of the view’s total width.
  3. The offset affects the leading edge of the image, meaning the image continues past that point. When the fraction nears 1.0, the image spills into the next view. You add a 20-point padding to the view’s trailing edge, providing a space for the image.

To use the new view, go to FlightCardView and replace the Spacer between DepartureTimeView and ArrivalTimeView with:

FlightProgressView(
  flight: flight,
  progress: flightTimeFraction(
    flight: flight
  )
)

Build and run the app, and you’ll see the progress indicator added to each flight.

Now that you have the underlying view in place, you can improve the view’s appearance. Open FlightCardView.swift and add the following at the end of the VStack:

.padding()
.background(
  Color.gray.opacity(0.3)
)
.clipShape(
  RoundedRectangle(cornerRadius: 20)
)
.overlay(
  RoundedRectangle(cornerRadius: 20)
    .stroke()
)
See forum comments
Cinema mode Download course materials from Github
Previous: Building Reusable Views Next: Using a View Builder