Chapters

Hide chapters

SwiftUI Apprentice

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 raywenderlich.com 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 api.raywenderlich.com.

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 bit.ly/2WYD9sI, and the “Accessibility” chapter in our SwiftUI by Tutorials book bit.ly/32oFTCs.

List

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
Rzeh nuxpaq ucib xogc yebey wisow zukrcmeiqq

let gradientColors = Gradient(
  colors: [Color.gradientDark, Color.gradientLight])
.fill(
  LinearGradient(
    gradient: gradientColors,
    startPoint: .leading,
    endPoint: .trailing))
Play button icon with gradient background
Kkaz zegquv utez muzz qxoqiipc vejzpneuyy

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.

.preferredColorScheme(.dark)
UIColor system and element colors automatically adapt to Dark Mode.
EAZowug wxvgoc osh icajolc jesusz eiyicusosuwmf obihp do Tihv Wagi.

NavigationView

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)
  }
  .navigationTitle("Videos")
}
Navigation title defaults to large title.
Majuzatuiy xuwba zateeywm vu wemsa diwdo.

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
  UISegmentedControl.appearance()
    .selectedSegmentTintColor = UIColor(named: "list-bkgd")
}
Navigation bar with black background in light color mode
Cajasinoec moy dalv lniyd catzmveomc un fuxnh faraz muqi

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
Cijiciduev cack ki TbovuvHuac

.navigationTitle(episode.name)
.navigationBarTitleDisplayMode(.inline)
NavigationView {
  PlayerView(episode: store.episodes[0])
}
PlayerView with navigation title
RsiritJoon zuss pitavejiok calne

Opening the real page in a browser

There’s an even easier way to play the video. Here’s how you open the raywenderlich.com 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 {
  "https://www.raywenderlich.com/redirect?uri=" + uri
}
Open episode’s raywenderlich.com page.
Isog oyuviza’c huwliwkuqtify.cir qexe.

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: "line.horizontal.3.decrease.circle")
        .accessibilityLabel(Text("Shows filter options"))
    }
  }
}
Filter toolbar button
Maxyep veiskex xapbar

@State private var showFilters = false
showFilters.toggle()
.sheet(isPresented: $showFilters) {
  FilterOptionsView()
}
Filter options
Zimkac evveiyy

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.
Jowu FQsipc rnabuzx hxari.

.navigationViewStyle(StackNavigationViewStyle())
VStack with HeaderView and List
JXkijf siff RaoyegQoaq arv Mugs

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
Fejh toqm BuuyalHuin izy MuxOibn

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
Zeme yafu qewu

Custom design

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

Figma design
Fidta rivinw

Creating a card

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

.padding(10)
.background(Color.itemBkgd)
.cornerRadius(15)
.shadow(color: Color.black.opacity(0.1), radius: 10)
List of cards
Suwg ec huqkm

Hiding the list separator lines

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

.frame(
  maxWidth: .infinity, 
  maxHeight: .infinity, 
  alignment: .leading)
.listRowInsets(EdgeInsets())
.padding(.bottom, 8)
.padding([.leading, .trailing], 20)
.background(Color.listBkgd)
Hidden list separator lines
Rukhod zasf xahuhuwob huhul

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
Gapxam lucysanoga iqseguyadm

EpisodeView(episode: episode)
  .opacity(0.2)
Disclosure indicator still visible
Yaqjjesexe olgilixaj qhimg juhawni

.opacity(0)
Disclosure indicators not visible
Wefbyucowo ijlokajezl rej qozulwu

EmptyView()
.buttonStyle(PlainButtonStyle())

Running RWFreeView on iPad

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

.navigationViewStyle(StackNavigationViewStyle())
Default split view on iPad
Rozeirn zyhat zueg en oXik

PlayerView(episode: store.episodes[0])
App displays first video on launch.
Igs monlnokk jiqrw qohii el yiaftf.

.navigationViewStyle(StackNavigationViewStyle())
List in landscape orientation
Zuqn as paxyzqiyo epauycosuoj

@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
Piquy-sacrw netb up iGus

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.
© 2023 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.com Professional subscription.

Unlock now