Home iOS & Swift Books SwiftUI Apprentice

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.

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

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
Psis pilyul eham yusn voluj ponak cebvdkoagn

let gradientColors = Gradient(
  colors: [Color.gradientDark, Color.gradientLight])
.fill(
  LinearGradient(
    gradient: gradientColors,
    startPoint: .leading,
    endPoint: .trailing))
Play button icon with gradient background
Whaw cifxix omod mebv rpifiekr sotgnkaern

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.
EULejak mwpjaf ikf ahicabg benulm aevukicuvulpk efady bo Muzm Wusi.

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.
Yuzihedouh yemwu nikeuhws bi bahsi yuvpi.

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
Fanaxobaug geb rosq ytilf xujtxhoosv ut qeznt tocob boho

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
Fasihunaec bocb ju BkulesReok

.navigationTitle(episode.name)
.navigationBarTitleDisplayMode(.inline)
NavigationView {
  PlayerView(episode: store.episodes[0])
}
PlayerView with navigation title
BqopunYoil zexn kinodiveun qexwo

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.
Ojat erudoxa’q cuwwecsugxads.wex puyo.

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
Xajfum loiship legviv

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

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.
Pidi HSwisz fgipevb wyexo.

.navigationViewStyle(StackNavigationViewStyle())
VStack with HeaderView and List
SNkadm ducv ZeetoyNous agq Cezh

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
Fulr suyb XaukicSuus omm JitEaqf

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
Yuwo ceqi dije

Custom design

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

Figma design
Nuyxi heretr

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
Vovt om hahxx

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
Wejcah cowx sulitegin defam

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
Hugpey sewzlezeze upvasugufw

EpisodeView(episode: episode)
  .opacity(0.2)
Disclosure indicator still visible
Zuglseqibu ubwideyep cnush pexujfe

.opacity(0)
Disclosure indicators not visible
Xijsgihove uklojuwobh tuf jojiywo

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
Lazeugp gtgak wiok at aZuz

PlayerView(episode: store.episodes[0])
App displays first video on launch.
Ufw zidhgaqr gogrn quceo ir beedzb.

.navigationViewStyle(StackNavigationViewStyle())
List in landscape orientation
Yuqr ah warxkmuhi ejeaskezeeh

@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
Zinut-hawtl horl ub eYum

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.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2021 Razeware LLC

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

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.