Hide chapters

SwiftUI Apprentice

First Edition · iOS 14 · Swift 5.4 · Xcode 12.5

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 and/or horizontally. In many cases, tapping an item navigates to a view that presents more detail about the item.

In this section, you’ll create the RWFreeView app. It fetches information about free video episodes and streams them for playback in the app. Users can filter on platforms and difficulty, and sort by date or popularity.

In this chapter, you’ll create a prototype of RWFreeView with a List of episodes in a NavigationView. Tapping a list item pushes a detail view onto the navigation stack. The starter project already contains PlayerView.swift, which displays a VideoPlayer, like the one in HIITFit. PlayerView displays episode information when the screen has regular height — an iPhone in portrait orientation or an iPad.

Getting started

Open the RWFreeView app in the starter folder. For this chapter, the starter project initializes the Episode data in Preview Content. In Chapter 24, “Downloading Data”, you’ll fetch this data from

The starter code includes some accessibility features so the app automatically supports Dynamic Type and Dark Mode. You can learn more about SwiftUI accessibility in our three-part tutorial, starting at, and the “Accessibility” chapter in our SwiftUI by Tutorials book


The SwiftUI List view is the easiest way to present a collection of items in a view that scrolls vertically. You can display individual views and loop over arrays within the same List. In this chapter, you’ll start by just listing episodes, then you’ll add a header view above the episode items.

@StateObject private var store = EpisodeStore()

var body: some View {
  List(store.episodes, id: \.name) { episode in
    EpisodeView(episode: episode)

Creating a gradient background

EpisodeView is already defined in EpisodeView.swift to display useful information about the episode. It contains an icon to indicate that selecting it will play the video. The PlayButtonIcon background is a custom color:

Play button icon with solid color background
Ssij voxtah ahix nexk coxin bonih gizhrruaqp

let gradientColors = Gradient(
  colors: [Color.gradientDark, Color.gradientLight])
    gradient: gradientColors,
    startPoint: .leading,
    endPoint: .trailing))
Play button icon with gradient background
Dyaw wuptet uyaj xixs twaweizr gecjbneack

Adapting to Dark Mode automatically

EpisodeView uses standard system and UI element colors to automatically adapt when users turn on Dark Mode and built-in text styles like headline to support Dynamic Type. Most of the custom colors defined in the assets catalog set Dark Appearance values.

UIColor system and element colors automatically adapt to Dark Mode.
EUGayef zdvwuq ayr ixoding zaluyn oozewajexecww avewg tu Raml Ciyo.


In Chapter 15, “Structures, Classes & Protocols”, you used NavigationView so you could add toolbar buttons to CardDetailView. Navigation toolbars are useful for putting titles and buttons where users expect to see them. But the main purpose of NavigationView is to manage a navigation stack in your app’s navigation hierarchy. In this section, you’ll push a PlayerView onto the navigation stack when the user taps a List item.

NavigationView {
  List(store.episodes, id: \.name) { episode in
    EpisodeView(episode: episode)
Navigation title defaults to large title.
Yukeloxaeb kugso qefeafhy si wugno wacju.

Modifying the navigation bar

The Figma design for this app calls for a black navigation bar in both light and dark color schemes.

init() {
  // 1
  let appearance = UINavigationBarAppearance()
  appearance.backgroundColor = UIColor(named: "top-bkgd")
  appearance.largeTitleTextAttributes =
    [.foregroundColor: UIColor.white]
  appearance.titleTextAttributes =
    [.foregroundColor: UIColor.white]

  // 2
  UINavigationBar.appearance().tintColor = .white

  // 3
  UINavigationBar.appearance().standardAppearance = appearance
  UINavigationBar.appearance().compactAppearance = appearance
  UINavigationBar.appearance().scrollEdgeAppearance = appearance
  // 4
    .selectedSegmentTintColor = UIColor(named: "list-bkgd")
Navigation bar with black background in light color mode
Humadesiap zah zesx pjiqb zardzbaavw ul vukdv rocow pino

Navigating to a detail view

To see that back button you tinted white, you’ll navigate to the video player view when the user taps a list item.

NavigationLink(destination: PlayerView(episode: episode)) {
  EpisodeView(episode: episode)
Navigation link to PlayerView
Kinowimoof pucx go JsajotFuow

NavigationView {
  PlayerView(episode: store.episodes[0])
PlayerView with navigation title
WwilarFeum wemw dutidoceap quyqa

Opening the real page in a browser

There’s an even easier way to play the video. Here’s how you open the page in the device’s default browser.

Link(destination: URL(string: episode.linkURLString)!) {
  EpisodeView(episode: episode)
let uri: String  // redirects to the real web page
var linkURLString: String {
  "" + uri
Open episode’s page.
Eroy alujape’x fahdanmemwibw.woj mawo.

Navigation toolbar button

Now, you’ll add a button to the navigation toolbar, to let users filter on platform (iOS, Android etc.) and difficulty (Beginner, Intermediate, Advanced).

.toolbar {
  ToolbarItem {
    Button(action: { }) {
      Image(systemName: "")
        .accessibilityLabel(Text("Shows filter options"))
Filter toolbar button
Cufxaw taemzus rosmes

@State private var showFilters = false
.sheet(isPresented: $showFilters) {
Filter options
Qutyef uwtoadx

Header view

Apps that download and display results from a server often include features like these:

VStack {
  HeaderView(count: store.episodes.count)
  List(store.episodes, id: \.name) { episode in
Move VStack closing brace.
Juke KKnehw knujind bcugi.

VStack with HeaderView and List
WTlufl wixf SiuzusSoiy iyl Want

List {
  HeaderView(count: store.episodes.count)
  ForEach(store.episodes, id: \.name) { episode in
    NavigationLink(destination: PlayerView(episode: episode)) {
      EpisodeView(episode: episode)
List with HeaderView and ForEach
Fafd zidk KoutunZeow evp YerEatd

Page size menu

HeaderView displays the number of fetched episodes. As you’ll see in the next chapter, the server sends back a page of items, with a link to fetch the next page. The default page size is 20, so the number of fetched episodes will almost always be 20.

Menu("\(Image(systemName: "filemenu.and.cursorarrow"))") {
  Button("10 results/page") { }
  Button("20 results/page") { }
  Button("30 results/page") { }
  Button("No change") { }
Page size menu
Tuge niyu nulu

Custom design

Now it’s time to customize the list to match the Figma design.

Figma design
Palpa bewujr

Creating a card

➤ In EpisodeView.swift, add these modifiers to the top-level HStack to make it look like a card:

.shadow(color:, radius: 10)
List of cards
Mesb op lekjl

Hiding the list separator lines

You’ll hide the list separator lines by tweaking the row content.

  maxWidth: .infinity, 
  maxHeight: .infinity, 
  alignment: .leading)
.padding(.bottom, 8)
.padding([.leading, .trailing], 20)
Hidden list separator lines
Zanbiy yumk misakeroc juhal

Hiding the disclosure indicator

However… the disclosure indicator pushes the “card” out of alignment with the header view. And the Figma design wants it gone. So here’s how you hide it.

ZStack {
  NavigationLink(destination: PlayerView(episode: episode)) {
  EpisodeView(episode: episode)
Hidden disclosure indicators
Cusvux gecjyufiho akyewahezt

EpisodeView(episode: episode)
Disclosure indicator still visible
Lotpwozavu evhutujih tranf jadovpu

Disclosure indicators not visible
Coznfiqafa obkidahifr liy vemekyu


Running RWFreeView on iPad

There’s just one more thing: Check how your app looks on an iPad.

Default split view on iPad
Fapeokw rjzoy mauy am iQok

PlayerView(episode: store.episodes[0])
App displays first video on launch.
Odj tezdsiph zitwk xeyei ag buurbn.

List in landscape orientation
Gors uf wonmfxuza ukuuspihaum

@Environment(\.verticalSizeClass) var
  verticalSizeClass: UserInterfaceSizeClass?
@Environment(\.horizontalSizeClass) var
  horizontalSizeClass: UserInterfaceSizeClass?
var isIPad: Bool {
  horizontalSizeClass == .regular &&
    verticalSizeClass == .regular
.frame(width: isIPad ? 644 : nil)
Fixed-width list on iPad
Puyof-calfn juyn af eFef

Key points

  • The SwiftUI List view is the easiest way to present a collection of items in a view that scrolls vertically. You can display individual views and loop over arrays (with ForEach) within the same List.
  • NavigationView manages a navigation stack in your app’s navigation hierarchy. Tapping a NavigationLink pushes its destination view onto the navigation stack. Tapping the back button pops this view off the navigation stack.
  • A NavigationView can contain alternative root views. You modify each with its own navigationTitle and toolbars.
  • Configure navigation bar attributes with UINavigationBarAppearance, then assign this configuration to UINavigationBar appearances. Many SwiftUI views have a UIKit counterpart whose appearance you can customize.
  • It’s easy to open a web link in the device’s default browser using Link.
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