Building Engaging User Interfaces with SwiftUI

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

Lesson 02: Implementing Complex UI Layouts

Making the Timeline Generic

Episode complete

Play next episode

Next

Heads up... You’re accessing parts of this content for free, with some sections shown as obfuscated text.

Heads up... You’re accessing parts of this content for free, with some sections shown as obfuscated text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

While you’ve allowed the user to pass in any view they wish, you still hard code the FlightInformation type in the view. Generics allow you to write code without being specific about the data type you’re using. You can write a function once and use it on any data type.

struct GenericTimelineView<Content, T>: View where Content: View, T: Identifiable {
let events: [T]
let content: (T) -> Content
init(
  events: [T],
  @ViewBuilder content: @escaping (T) -> Content
) {
  self.events = events
  self.content = content
}
return GenericTimelineView(events: testFlights) { flight in
  FlightCardView(flight: flight)
}
ScrollView {
  VStack {
    ForEach(events) { flight in
      content(flight)
    }
  }
}
GenericTimelineView(events: flights) { flight in

Using Key Paths

A key path lets you refer to a property on an object. That’s not the same as the value of the property, as a key path represents the property itself. You use them quite often in SwiftUI. You’ve probably already used them if you’ve ever written code similar to:

ForEach(stores.indices, id: \.self) { index in
let timeProperty: KeyPath<T, Date>
init(
  events: [T],
  timeProperty: KeyPath<T, Date>,
  @ViewBuilder content: @escaping (T) -> Content
) {
  self.events = events
  self.content = content
  self.timeProperty = timeProperty
}
return GenericTimelineView(
  events: testFlights,
  timeProperty: \.localTime
) { flight in
  FlightCardView(flight: flight)
}
var earliestHour: Int {
  let flightsAscending = events.sorted {
    // 1
    $0[keyPath: timeProperty] < $1[keyPath: timeProperty]
  }

  // 2
  guard let firstFlight = flightsAscending.first else {
    return 0
  }
  // 3
  let hour = Calendar.current.component(
    .hour,
    from: firstFlight[keyPath: timeProperty]
  )
  return hour
}
var latestHour: Int {
  let flightsAscending = events.sorted {
    $0[keyPath: timeProperty] > $1[keyPath: timeProperty]
  }

  guard let firstFlight = flightsAscending.first else {
    return 24
  }
  let hour = Calendar.current.component(
    .hour,
    from: firstFlight[keyPath: timeProperty]
  )
  return hour + 1
}
func eventsInHour(_ hour: Int) -> [T] {
  return events
    .filter {
      let flightHour =
        Calendar.current.component(
          .hour,
          from: $0[keyPath: timeProperty]
        )
      return flightHour == hour
    }
}
func hourString(_ hour: Int) -> Date {
  let tcmp = DateComponents(hour: hour)
  guard let time = Calendar.current.date(from: tcmp) else { return Date() }
  return time
}
ScrollView {
  VStack(alignment: .leading) {
    // 1
    ForEach(earliestHour..<latestHour, id: \.self) { hour in
      // 2
      let hourFlights = eventsInHour(hour)
      // 3
      Text(hourString(hour), style: .time)
        .font(.title2)
      // 4
      ForEach(hourFlights.indices, id: \.self) { index in
        content(hourFlights[index])
      }
    }
  }
}
GenericTimelineView(
  events: flights,
  timeProperty: \.localTime) { flight in
    FlightCardView(flight: flight)
}
See forum comments
Cinema mode Download course materials from Github
Previous: Using a View Builder Next: Conclusion