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

14. Lists
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.

Most apps focus on displaying some type of data to the user. Whether upcoming appointments, past orders or new products, you must clearly show the user the information they come to your app for.

In the previous chapter, you saw a preview of iterating through data when displaying the flights for a day and allowing the user to interact with this data. In this chapter, you’ll dive deeper into the ways SwiftUI provides you to show a list of items to the user of your app.

Iterating Through Data

Open the starter project for this chapter and go to FlightList.swift in the FlightStatusBoard group. You’ll see a slightly different view than the one you created in the previous chapter. In place of List, which you’ll work with later in this chapter, you’ll start by examining ForEach.

SwiftUI uses ForEach as a fundamental element to loop over data. When you pass it a collection of data, it then creates multiple sub-views using a provided closure, one for each data item. ForEach works with any type of collected data. You can think of ForEach as the SwiftUI version of the for-in loop in traditional Swift code.

Run the app, tap Flight Status — and you’ll notice a mess.

Bad schedule
Bad schedule

Remember that ForEach operates as an iterator. It doesn’t provide any structure. As a result, you’ve created many views but not provided a layout for them. They’re all at the top level, not contained in anything else. SwiftUI tries to fit everything together, but the result is a mess. To fix both issues, add some structure around the loop:

ScrollView {
  VStack {
    ForEach(flights, id: \.id) { flight in
      NavigationLink(value: flight) {
        FlightRow(flight: flight)
      }
    }
    .navigationDestination(
      for: FlightInformation.self,
      destination: { flight in
        FlightDetails(flight: flight)
      }
    )
  }
}

You wrapped the ForEach loop inside a VStack — giving you a vertical stack of rows — and a ScrollView — that allows scrolling the rows since there’s more content than will fit onto the view. SwiftUI picks up that you’ve wrapped a VStack and applies vertical scrolling to match. If a line of text within the view became longer than the view’s width, SwiftUI wouldn’t automatically add horizontal scrolling.

Scrolled list
Scrolled list

You can override this default scrolling direction by passing in the desired scroll axes to ScrollView. To scroll the view in both directions, you would change the call to:

ScrollView([.horizontal, .vertical]) {

ScrollView provides a useful, general way to let a user browse through data that won’t fit onto a single screen.

Also note the id: parameter is passed a keypath to a property of the type in the array. This parameter hints that SwiftUI has expectations for the data sent to an iteration. In the next section, you’ll explore these expectations and make your data work more smoothly with SwiftUI.

Making Your Data Work Better With Iteration

The data passed into ForEach must provide a way to identify each element of the array as unique. In this loop, you use the id: parameter to tell SwiftUI to use the \.id property of FlightInformation as the unique identifier for each element in the array.

extension FlightInformation: Identifiable {
}
ForEach(flights) { flight in
List Functions as Before with Identifiable
Wayl Zowpbeafd iq Hoyewa woff Esimreheocde

Improving Performance

When a VStack or HStack renders, SwiftUI creates all the cells at once. For a view such as this with only thirty rows, that probably doesn’t matter. For rows with hundreds of potential rows, that’s a waste of resources since most are not visible to the user. Using the Lazy versions of these stacks introduced in SwiftUI 2.0 (iOS 14, macOS 11, etc.) provides a quick performance improvement when iterating over large data sets.

Lazy vs not
Lidy cr rim

Setting the Scroll Position in Code

A major weakness of the first version of SwiftUI was the lack of a way to set the scrolling position programmatically. SwiftUI 2.0, introduced with iOS 14 and macOS Big Sur, added ScrollViewReader that allows setting the current position from code. You’ll use it to scroll the flight status list to the next flight automatically. Replace the current ScrollView with the following code:

ScrollViewReader { scrollProxy in
  ScrollView {
    LazyVStack {
      ForEach(flights) { flight in
        NavigationLink(value: flight) {
          FlightRow(flight: flight)
        }
      }
      .navigationDestination(
        for: FlightInformation.self,
        destination: { flight in
          FlightDetails(flight: flight)
        }
      )
    }
  }
  // onAppear()
}
var nextFlightId: Int {
  guard let flight = flights.first(
    where: {
      $0.localTime >= Date()
    }
  ) else {
    return flights.last?.id ?? 0
  }
  return flight.id
}
.onAppear {
  scrollProxy.scrollTo(nextFlightId)
}
Positioning List to the Next Flight
Tocediunuvk Xosg pi qma Lurp Kmiwxy

scrollProxy.scrollTo(nextFlightId, anchor: .center)
Late view
Vuha leus

Creating Lists

SwiftUI provides the List struct that does the heavy lifting for you and uses the platform-specific control to display the data. A List is a container much like a VStack or HStack that you can populate with static views, dynamic data or other iterative views.

ScrollViewReader { scrollProxy in
  List(flights) { flight in
    NavigationLink(value: flight) {
      FlightRow(flight: flight)
    }
  }
  .navigationDestination(
    for: FlightInformation.self,
    destination: { flight in
      FlightDetails(flight: flight)
    }
  )
  .onAppear {
    scrollProxy.scrollTo(nextFlightId, anchor: .center)
  }
}
Flight list
Zfekbc jotr

Building Search Results

To start building the search view, open SearchFlights.swift under the SearchFlights group. You’ll see view to allow the user to search for flights. However, the search functionality isn’t in place yet. In this section, you’re going to fix that.

@State private var city = ""
.searchable(text: $city)
New Search Field
Cen Sautnx Huehy

if !city.isEmpty {
  matchingFlights = matchingFlights.filter {
    $0.otherAirport.lowercased().contains(city.lowercased())
  }
}
List(matchingFlights) { flight in
  SearchResultRow(flight: flight)
}
Searching flights
Duoxyqugv xvadjzs

Building a Hierarchical List

SwiftUI added support for displaying hierarchical data in version 2.0. Much as the NavigationLink gives you a structure to organize views from general to more specific, a hierarchical list gives you an excellent way to display data that moves from general to more specific. In this section, you will update the search results into a hierarchical list that displays dates and then displays the flights for that date under it.

Hierarchical list
Yoobidmqelus vuwm

struct HierarchicalFlightRow: Identifiable {
  var label: String
  var flight: FlightInformation?
  var children: [HierarchicalFlightRow]?

  var id = UUID()
}
func hierarchicalFlightRowFromFlight(_ flight: FlightInformation)
  -> HierarchicalFlightRow {
  return HierarchicalFlightRow(
    label: longDateFormatter.string(from: flight.localTime),
    flight: flight,
    children: nil
  )
}
var flightDates: [Date] {
  let allDates = matchingFlights.map { $0.localTime.dateOnly }
  let uniqueDates = Array(Set(allDates))
  return uniqueDates.sorted()
}
func flightsForDay(date: Date) -> [FlightInformation] {
  matchingFlights.filter {
    Calendar.current.isDate($0.localTime, inSameDayAs: date)
  }
}
var hierarchicalFlights: [HierarchicalFlightRow] {
  // 1
  var rows: [HierarchicalFlightRow] = []

  // 2
  for date in flightDates {
    // 3
    let newRow = HierarchicalFlightRow(
      label: longDateFormatter.string(from: date),
      // 4
      children: flightsForDay(date: date).map {
        hierarchicalFlightRowFromFlight($0)
      }
    )
    rows.append(newRow)
  }
  return rows
}
// 1
List(hierarchicalFlights, children: \.children) { row in
  // 2
  if let flight = row.flight {
    SearchResultRow(flight: flight)
  } else {
    Text(row.label)
  }
}
Hierarchical flight list
Niufulcziwag pbitsy zest

Grouping List Items

A long list of data can be challenging for the user to read. Fortunately, the List view supports breaking a list into sections. Combining dynamic data and sections moves into some more complex aspects of displaying data in SwiftUI. In this section, you’ll separate flights into sections by date and add a header and footer to each section.

// 1
List {
  // 2
  ForEach(flightDates, id: \.hashValue) { date in
    // 3
    Section(
      // 4
      header: Text(longDateFormatter.string(from: date)),
      // 5
      footer:
        Text(
          "Matching flights " + "\(flightsForDay(date: date).count)"
        )
        .frame(maxWidth: .infinity, alignment: .trailing)
    ) {
      // 6
      ForEach(flightsForDay(date: date)) { flight in
        SearchResultRow(flight: flight)
      }
    }
  }
}
// 7
.listStyle(InsetGroupedListStyle())
Grouped
Vmeepac

Key Points

  • A ScrollView wraps a view within a scrollable region that doesn’t affect the rest of the view.
  • The ScrollViewProxy lets you change the current position of a list from code.
  • SwiftUI provides two ways to iterate over data. The ForEach option loops through the data allowing you to render a view for each element.
  • A List uses the platform’s list control to display the elements in the data.
  • Data used with ForEach and List must provide a way to identify each element uniquely. You can do this by specifying an attribute that implements the Hashable protocol, have the object implement Hasbable and pass it to the id parameter or have your data implement the Identifiable protocol.
  • Building a hierarchical view requires a hierarchical data structure to describe how the view should appear.
  • You can split a List in Sections to organize the data and help the user understand what they see.
  • You can combine ForEach and List to create more complex data layouts. This method works well when you want to group data into sections.

Where to Go From Here?

For more on integrating navigation and views, look at SwiftUI Tutorial: Navigation at https://www.kodeco.com/5824937-swiftui-tutorial-navigation.

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