Chapters

Hide chapters

SwiftUI Apprentice

Second Edition · iOS 16 · Swift 5.7 · Xcode 14.2

Section I: Your First App: HIITFit

Section 1: 12 chapters
Show chapters Hide chapters

Section II: Your Second App: Cards

Section 2: 9 chapters
Show chapters Hide chapters

22. Lists & Navigation
Written by Audrey Tam

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

Most apps have at least one view that displays a collection of similar items in a table or grid. When there are too many items to fit on one screen, the user can view more items by scrolling — vertically, horizontally or both. In many cases, tapping an item navigates to a view that presents more detail about the item.

In this section, you’ll start implementing TheMet, an app that searches The Metropolitan Museum of Art, New York for objects matching the user’s query term.

In this chapter, you’ll create a prototype of TheMet with a List of objects in a NavigationStack. Tapping a list item pushes a detail view onto the navigation stack. The starter project already contains ObjectView.swift, which displays some of the object properties.

Getting Started

➤ Open the TheMet app in the starter folder. For this chapter, the starter project initializes the Object data in Preview Content. In Chapter 24, “Downloading Data”, you’ll fetch this data from collectionapi.metmuseum.org.

List

You encountered the SwiftUI List view in Chapter 10, “Working With Datasets”, where you learned how to let users edit the history of exercises in HIITFit.

@StateObject private var store = TheMetStore()

var body: some View {
  List(store.objects, id: \.objectID) { object in
    Text(object.title)
  }
}
List with development data
Goqw pusw xuhikefject hula

NavigationStack

In Chapter 13, “Outlining a Photo Collage App”, you used NavigationStack so you could add toolbar buttons to SingleCardView. Navigation toolbars are useful for putting titles and buttons where users expect to see them. But the main purpose of NavigationStack is to manage a navigation stack in your app’s navigation hierarchy. In this section, you’ll push an ObjectView onto the navigation stack when the user taps a List item.

NavigationStack {
  List(store.objects, id: \.objectID) { object in
    Text(object.title)
  }
  .navigationTitle("The Met")
}
Navigation title defaults to large title.
Qopitiruad vagmu dofuuhlz qe kalgo pupqo.

Navigating to a Detail View

Now, you’ll navigate to the object’s ObjectView when the user taps the list item.

NavigationLink(object.title) {
  ObjectView(object: object)
}
Navigation link to ObjectView
Fediwoveok qufs ni EzxibkJooz

Using the Internet

You’ll soon learn how to download data from an internet server into your app, but first you’ll see a couple of ways to use the device’s default browser.

Link Button

➤ In ContentView.swift, comment out the NavigationLink(...) { ... } code and type the following code:

Link(object.title, destination: URL(string: object.objectURL)!)
Open object's metmuseum.org page in browser app.
Adus enfebk'y zemximiip.obt pera up wrufruw iqx.

SFSafariViewController

You might prefer your users don’t leave your app. You can open a Safari browser in your app. Your users can tap links on the page, but they can’t wander away from your app by entering their own URLs.

import SwiftUI
import SafariServices

struct SafariView: UIViewControllerRepresentable {
  let url: URL

  func makeUIViewController(
    context: UIViewControllerRepresentableContext<SafariView>
  ) -> SFSafariViewController {
    return SFSafariViewController(url: url)
  }

  func updateUIViewController(
    _ uiViewController: SFSafariViewController,
    context: UIViewControllerRepresentableContext<SafariView>) {}
}
NavigationLink(
  destination: SafariView(url: URL(string: object.objectURL)!)) {
  HStack {
    Text(object.title)
    Spacer()
    Image(systemName: "rectangle.portrait.and.arrow.right.fill")
      .font(.footnote)
  }
}
Open object's metmuseum.org page in Safari, in your app.
Eyuz uclabs'b sidvayiaw.esz jifu iq Giqeju, ol luaw ajf.

AsyncImage

In the next chapters, you’ll learn how to use URLSession methods to download Object data from metmuseum.org, but it’s quick and easy to download and display an image with the AsyncImage view.

AsyncImage(url: URL(string: object.primaryImageSmall)) { image in
  image
    .resizable()
    .aspectRatio(contentMode: .fit)
} placeholder: {
  PlaceholderView(note: "Display image here")
}
Download and display the object's primary image.
Xobhcaes opt vebvvuj hyi eccanq'j jwizokb aduzo.

navigationDestination

This app can download two kinds of objects from metmuseum.org. Those in the public domain have a primary image you can easily display in an ObjectView. What should your app do for objects that aren’t in the public domain? Well, you can just as easily load their web page into a SafariView. In this section, you’ll see how to set up navigation destinations for different types of value.

Extracting Web Indicator View

➤ First, in ContentView.swift, to keep your code neat, extract the HStack with the web indicator into its own view:

struct WebIndicatorView: View {
  let title: String

  var body: some View {
    HStack {
      Text(title)
      Spacer()
      Image(systemName: "rectangle.portrait.and.arrow.right.fill")
        .font(.footnote)
    }
  }
}
NavigationLink(destination: SafariView(url: URL(string: object.objectURL)!)) {
  WebIndicatorView(title: object.title)
}

Handling Both Kinds of Objects

➤ Now, comment out the SafariView navigation link and uncomment the ObjectView navigation link:

NavigationLink(object.title) {
  ObjectView(object: object)
}
Image not in public domain.
Ulema fuj ax gibjer muyaar.

if !object.isPublicDomain,
   let url = URL(string: object.objectURL) {
  NavigationLink(destination: SafariView(url: url)) {
    WebIndicatorView(title: object.title)
  }
} else {
  NavigationLink(object.title) {
    ObjectView(object: object)
  }
}
Navigate to ObjectView or SafariView.
Mugefaki je IzyecwMeaf ep WerosiRiaf.

Using navigationDestination

➤ Replace your if-else code with the following:

if !object.isPublicDomain,
   let url = URL(string: object.objectURL) {
  NavigationLink(value: url) {
    WebIndicatorView(title: object.title)
  }
} else {
  NavigationLink(value: object) {
    Text(object.title)
  }
}
.navigationDestination(for: URL.self) { url in
  SafariView(url: url)
    .navigationBarTitleDisplayMode(.inline)
    .ignoresSafeArea()
}
.navigationDestination(for: Object.self) { object in
  ObjectView(object: object)
}

Testing for Invalid objectURL

Now that you’re checking for a valid URL before calling SafariView(url:), how do you test for an invalid URL? Well, you’ve got your own sample data in MetStoreDevData.swift, and you can set any values you like.

objectURL: "",    // don't forget the comma!
List: Non-public-domain object with invalid URL
Lect: Wug-yiykaf-juyiac azroqn mawm altonen IJD

ObjectView: Non-public-domain object with invalid URL
IrsahsJuup: Qem-bopkoq-jocuih ufbuzd kodb ipduyem APM

PlaceholderView(note: "Image not in public domain. URL not valid.")
ObjectView: Non-public-domain object with invalid URL
OydinfJuim: Yuw-cixrob-hokoih uqzocz pixw afyocah IKF

Using Custom Colors

➤ In MetStoreDevData.swift, restore the oil lamp’s objectURL, go back to ContentView and tap through to its SafariView. Then tap THE MET (on the web page) to go to the home page:

Metropolitan Museum home page
Reyyuraxihac Yunaep yuno yeva

Color-Coding the List Rows

➤ Add these modifiers to the NavigationLink of the non-public-domain objects:

.listRowBackground(Color.metBackground)
.foregroundColor(.white)
.listRowBackground(Color.metForeground)
Color-coded list rows
Yamal-hucil gapy gaqn

Linking to Met From ObjectView

Tapping a public-domain object navigates to its ObjectView, which downloads and displays its primary image. A natural UX improvement is to provide a button here to open the object’s metmuseum.org page.

if let url = URL(string: object.objectURL) {
  Link(destination: url) {
    WebIndicatorView(title: object.title)
      .multilineTextAlignment(.leading)
      .font(.callout)
      .frame(minHeight: 44)
      // add these four modifiers
      .padding()
      .background(Color.metBackground)
      .foregroundColor(.white)
      .cornerRadius(10)
  }
} else {
  Text(object.title)
    .multilineTextAlignment(.leading)
    .font(.callout)
    .frame(minHeight: 44)
}
Link to metmuseum.org matches non-public-domain list rows.
Duyd ku lihzuzooy.uzw micfjap wuq-lolgid-fecuuk depk negj.

One Last Thing

Soon, you’ll implement the code to download objects from metmuseum.org. These objects will match the user’s query term, like “rhino” or “persimmon”. To prepare for that, you’ll add a button that will show an alert where the user can enter a query term.

@State private var query = "rhino"
@State private var showQueryField = false
.toolbar {
  Button("Search the Met") {
    query = ""
    showQueryField = true
  }
  .foregroundColor(Color.metBackground)
  .padding(.horizontal)
  .background(
    RoundedRectangle(cornerRadius: 8)
      .stroke(Color.metBackground, lineWidth: 2))
}
Search button in toolbar
Qaogrq fovhir il diopqum

.alert("Search the Met", isPresented: $showQueryField) {
  TextField("Search the Met", text: $query)
  Button("Search") { }
}
Alert with text field to get query term
Avozy levx gimz haesr la cul feaxd nivx

The Very Last Thing

Users expect to see a reminder of what they searched for, so you’ll add a message above the list.

Text("You searched for '\(query)'")
  .padding(5)
  .background(Color.metForeground)
  .cornerRadius(10)
You-searched-for message
Sua-paekjmot-vod fiwfawa

Key Points

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