
Just build something that you'd want to use today, not something you think people would use somehow.
– Paul Graham
首先,什麼是「導覽視圖」(Navigation View)?和清單視圖類似,導覽視圖是 iOS App 中常用的 UI 元件,它們提供用來顯示分層內容的階層介面。查看預先安裝的相片 App、YouTube 以及聯絡人,它們都利用導覽視圖以分層方式顯示內容。通常,你會將導覽視圖與一堆清單視圖結合,來為你的 App 建立複雜的介面,但請務必注意,這並不表示你必須同時使用兩者,導覽視圖可以和任何類型的視圖一起運用。
我們回到 FoodPin 專案 (http://www.appcoda.com/resources/swift59/swiftui-foodpin-list-deletion.zip) 並開啟 RestaurantListView. swift 檔。
按住 control 鍵並點選 RestaurantListView 中的 List,在內容選單中選擇「Embed...」,然後變更預設容器(Container )為「NavigationStack」,如圖 11.1 所示。在 iOS 中,你使用 NavigationStack 來建立導覽視圖。

要設定導覽列的標題,則在導覽堆疊(Navigation Stack )中加入 .navigationTitle 修飾器。更改後的程式碼如下所示:
NavigationStack {
List {
.
.
.
}
.listStyle(.plain)
.navigationTitle("FoodPin")
.navigationBarTitleDisplayMode(.automatic)
}
或者,你可以使用 .navigationBarTitleDisplayMode 修飾器來設定導覽列(Navigation Bar)的顯示模式。透過將其設定為「.automatic」,你可以讓 iOS 確定導覽列的適當大小; 如果你想要固定導覽列的大小,則可以將其設定為「.large」或「.inline」。
完成變更後,預覽應該會渲染導覽視圖,App 使用者介面的整體外觀基本上保持不變, 除了你現在應該會看到一個帶有大標題的導覽列,如圖 11.2 所示。如果你執行 App 並滾動清單,導覽列會自動最小化,這行為說明了 .automatic 模式的工作原理。

接下來,我們結合餐廳的細節視圖(Detail View ),當使用者點擊任何項目時,App 會導覽到另一個顯示餐廳細節的視圖,圖 11.3 顯示了預期的 UI。

我們會建立一個新檔案,並在其中實作細節視圖。在專案導覽器中的「View」資料夾上按右鍵,並選擇「New File...」,然後選取「SwiftUI View」模板,將檔案命名為「RestaurantDetailView.swift」。
細節視圖是設計用來顯示餐廳資訊,因此我們先宣告一個變數來存放餐廳物件:
var restaurant: Restaurant
當你新增變數後,Xcode 會在 #Previews 巨集中顯示錯誤。我們需要更新 RestaurantDetail View 的初始化,並傳送一個餐廳範例給它,如下所示:
RestaurantDetailView(restaurant: Restaurant(name: "Cafe Deadend", type: "Cafe", location: "Hong Kong", image: "cafedeadend", isFavorite: true))
現在我們來實作細節視圖的佈局。我們從背景圖片開始如何呢?你應該已經知道如何使用 Image 視圖來載入圖片,因此替換 body 變數如下:
var body: some View {
Image(restaurant.image)
.resizable()
}
透過套用 resizable 修飾器,圖片會被拉伸來填滿視圖,但是它不會保持長寬比。除此之外,還有一個問題,即圖片沒有完全填滿整個螢幕,如圖 11.4 所示。

透過套用 resizable 修飾器,圖片會被拉伸來填滿視圖,但是它不會保持長寬比。除此之外,還有一個問題,即圖片沒有完全填滿整個螢幕,如圖 11.4 所示。
Image(restaurant.image)
.resizable()
.scaledToFill()
.frame(minWidth: 0, maxWidth: .infinity)
.ignoresSafeArea()
frame 修飾器用來確定圖片框的大小;將值設定為 .infinity,表示圖片應占滿螢幕的整個寬度,如圖 11.5 所示。

好的,我們已經實作了背景圖片,那我們如何將餐廳資訊框覆蓋在圖片之上呢?答案是使用堆疊視圖。
到目前為止,我已經向你介紹了 HStack 與 VStack,不過 SwiftUI 中還有另一種名為「ZStack」的堆疊視圖,透過使用 ZStack,你可以輕鬆將視圖覆蓋在另一個視圖之上。
現在更新 body 中的程式碼如下:
ZStack {
Image(restaurant.image)
.resizable()
.scaledToFill()
.frame(minWidth: 0, maxWidth: .infinity)
.ignoresSafeArea()
Color.black
.frame(height: 100)
.opacity(0.8)
.clipShape(RoundedRectangle(cornerRadius: 20))
.padding()
}
要實現這種覆蓋效果,我們將 Image 視圖封裝在 ZStack 中,並引入一個新的 Color 視圖。在 ZStack 中的視圖順序決定了它們的分層,在這種情況下,Color 視圖會放置在 Image 視圖之上以建立所需的覆蓋效果。
對於 Color 視圖,我們指定黑色並將其框的高度設定為「100 點」。為了實現半透明的外觀,我們應用 opacity 修飾器,並將其值設定為「0.8」,變更完成後,預覽應該會在螢幕中央顯示一個圓角矩形,如圖 11.6 所示。

接下來,我們如何將矩形框移到螢幕頂部呢?與 HStack(或VStack )類似,ZStack 帶有 alignment 參數,你可以更改 ZStack 的初始化如下:
ZStack(alignment: .top) {
.
.
.
}
透過設定 alignment 的值為「.top」,矩形框會移動到螢幕頂部,如圖 11.7 所示。

要顯示餐廳資訊(包括名稱、類型與位置),我們可以將 overlay 修飾器加到 Color 視圖:
Color.black
.frame(height: 100)
.opacity(0.8)
.clipShape(RoundedRectangle(cornerRadius: 20))
.padding()
.overlay {
VStack(spacing: 5) {
Text(restaurant.name)
Text(restaurant.type)
Text(restaurant.location)
}
.font(.system(.headline, design: .rounded))
.foregroundStyle(.white)
}
我們使用 overlay 修飾器來覆蓋 Color 視圖中的內容。為了顯示餐廳資訊,我們使用 VStack 來顯示餐廳名稱、類型與位置,如圖 11.8 所示。

現在我們已經建立了細節視圖,下一步是啟用從清單視圖到細節視圖的導覽。要實現此目的,則開啟RestaurantListView.swift 檔,並修改 RestaurantListView 結構。使用 NavigationLink 包裹 BasicTextImageRow 視圖,如下所示:
NavigationLink(destination: RestaurantDetailView(restaurant: restaurants[index])) {
BasicTextImageRow(restaurant: $restaurants[index])
}
為了啟用清單視圖中所有項目的導覽,我們利用 NavigationLink 元件。NavigationLink 的 destination 參數指定導覽的目標視圖, 在本例中, 我們將其設定為最近建立的 RestaurantDetailView,如圖 11.9 所示。
執行 App 來進行測試,點擊任何一間餐廳,將會導覽到細節視圖。

預設上,「返回」按鈕顯示為藍色,如果你想要將其變更為其他顏色,可以將 .tint 修飾器加到 NavigationStack,並指定你喜愛的顏色。以下是一個例子:
NavigationStack {
.
.
.
}
.tint(.white)
系統會自動產生「返回」按鈕,該按鈕由由朝左的V 形圖示和原始視圖的標題(本例中是 FoodPin )組成在 SwiftUI 中,自訂「返回」按鈕的一種方式是建立我們自己的按鈕, 例如:如果我們想在「返回」按鈕中顯示餐廳名稱,該怎麼做呢?

你可以將下列的修飾器加到 RestaurantDetailView 的 ZStack 視圖中:
ZStack(alignment: .top) {
.
.
.
}
.navigationBarBackButtonHidden(true)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
dismiss()
}) {
Text("\(Image(systemName: "chevron.left")) \(restaurant.name)")
}
}
}
為了隱藏原始的「返回」按鈕,我們使用 navigationBarBackButtonHidden 修飾器,然後我們使用toolbar 修飾器來建立「返回」按鈕的自訂版本。在 toolbar 修飾器中,我們定義一個 ToolbarItem 充當按鈕,顯示餐廳名稱,且我們將 ToolbarItem 的位置設定為導覽列的前緣。
由於「返回」按鈕是手動建立的,因此我們需要自行實作按鈕的動作,這就是為什麼我們在 action 閉包中編寫以下這行程式碼:
dismiss()
不過,這個 dismiss() 函數是什麼呢?我們還沒有實作它,因此 Xcode 應該會顯示一些錯誤。為了讓程式正常運作,在 RestaurantDetailView 中宣告 dismiss 變數:
@Environment(\.dismiss) var dismiss
SwiftUI 框架提供一個名為「@Environment」的屬性包裹器,以供開發者從視圖的環境中讀取值,例如:你可以讀取視圖中使用的配色方案(深色 / 淺色模式)的值。.dismiss 是用來關閉目前視圖的環境值,如圖 11.11 所示。

在 iOS 15 中首次導入了dismiss 環境值,如果你的 App 支援較低版本的 iOS,你可以將其替換為presentationMode:
@Environment(\.presentationMode) var presentationMode
並且,你可以像這樣呼叫 dismiss():
presentationMode.wrappedValue.dismiss()
加入導覽連結後,系統會自動為清單視圖中的每個項目包含一個揭示指示器(Disclosure Indicator ),如果你希望移除揭示指示器,SwiftUI 並沒有提供特定的修飾器來控制其可見性,但是你可以使用 ZStack 與 EmptyView 來停用揭示指示器。

回到 RestaurantListView.swift,在 RestaurantListView 結構中,將下列程式碼:
NavigationLink(destination: RestaurantDetailView(restaurant: restaurants[index])) {
BasicTextImageRow(restaurant: restaurant)
}
改為:
ZStack(alignment: .leading) {
NavigationLink(destination: RestaurantDetailView(restaurant: restaurants[index])) {
EmptyView()
}
.opacity(0)
BasicTextImageRow(restaurant: $restaurants[index])
}
更改後,再次執行 App 來進行測試,揭示指示器應該消失了。
在本章中,我引導你了解導覽視圖與導覽連結的基礎知識。使用 SwiftUI,則你只需幾行程式碼就能輕鬆建立基於導覽的使用者介面,我還解釋了一些自訂導覽列的技術。此外,透過實作細節視圖,你現在應該熟悉如何使用 ZStack 來將一個視圖覆蓋在另一個視圖之上。
在本章所準備的範例檔中,有最後完整的 Xcode 專案可供你下載參考:
http://www.appcoda.com/resources/swift59/swiftui-foodpin-navigation.zip 。