在大多數的 App 中(尤其是以內容為基礎的 App),你應該體驗過導覽介面。這類型的 UI 通常有一個包含資料清單的導覽列,並且它讓使用者點擊內容時導覽至細節視圖。
在 UIKit 中,我們可以使用 UINavigationController 來實作這類型的介面。在 SwiftUI 中,Apple 稱其為「NavigationView」。由 iOS 16 開始,這個 「NavigationView」 以 「NavigationStack」取替。在本章中,我詳細解說導覽 UI 的實作,並教你如何進行一些自定義。和往常一樣,我們將進行幾個範例專案,以讓你獲得一些使用 NavigationStack 的實務經驗。

讓我們開始並實作一個我們之前使用導覽 UI 建立的範例專案。那麼,首先至下列網址下載起始專案:https://www.appcoda.com/resources/swiftui5/SwiftUINavigationListStarter.zip 。下載後開啟專案,並看一下預覽,你應該對於這個範例 App 非常熟悉,它只顯示一個餐廳列表,如圖 11.2 所示。

我們所要做的是,將這個清單視圖嵌入至導覽視圖中。
在舊版的 iOS,SwiftUI 框架提供一個名為 NavigationView 的視圖來建立導覽 UI。要將清單視圖嵌入至NavigationView 中,你所需要做的是使用 NavigationView 包裹 List ,如下所示:
NavigationView {
List {
ForEach(restaurants) { restaurant in
BasicImageRow(restaurant: restaurant)
}
}
.listStyle(.plain)
}
在 iOS 16 中,Apple 將 NavigationView 替換為 NavigationStack。 你仍然可以使用 NavigationView 來創建導航視圖, 但建議使用 NavigationStack,因為 NavigationView 最終會從 SDK 中移除。
要使用 NavigationStack 創建導航視圖,你可以將以上的程式碼寫成這樣:
NavigationStack {
List {
ForEach(restaurants) { restaurant in
BasicImageRow(restaurant: restaurant)
}
}
.listStyle(.plain)
}
進行更改後,你應該會看到一個空的導航欄。 要為欄加入標題,請使用 navigationBarTitle 修飾符,如下所示:
NavigationStack {
List {
ForEach(restaurants) { restaurant in
BasicImageRow(restaurant: restaurant)
}
}
.listStyle(.plain)
.navigationTitle("Restaurants")
}
現在,該 App 應該有一個具大標題的導覽列,如圖 11.3 所示。

至目前為止,我們只是在清單視圖中加入一個導覽列。我們通常使用導覽介面來讓使用者導覽至細節視圖,以顯示所選項目的細節。對於此範例,我們將建立一個簡單的細節視圖,以顯示餐廳的大圖,如圖 11.4 所示。

讓我們從細節視圖開始。在 ContentView.swift 檔的結尾處,插入下列的程式碼,以建立細節視圖:
struct RestaurantDetailView: View {
var restaurant: Restaurant
var body: some View {
VStack {
Image(restaurant.image)
.resizable()
.aspectRatio(contentMode: .fit)
Text(restaurant.name)
.font(.system(.title, design: .rounded))
.fontWeight(.black)
Spacer()
}
}
}
細節視圖就像 View型別的其他 SwiftUI 視圖一樣,它的佈局非常簡單,只顯示餐廳的圖片及名稱。RestaurantDetailView 結構還帶入一個 Restaurant 物件,以檢索餐廳的圖片及名稱。
好的,細節視圖已經準備就緒,問題是你如何將內容視圖中所選的餐廳傳送至此細節視圖呢?
SwiftUI 提供一個名為 NavigationLink 的特殊按鈕,它能夠偵測使用者的觸控,並觸發導覽顯示,NavigationLink 的基本用法如下:
NavigationLink(destination: DetailView()) {
Text("Press me for details")
}
你可在 destination 參數中指定目標視圖,並在閉包中實作其外觀。對於範例 App, 應該在點擊任何一間餐廳時,導覽至細節視圖。在這個範例中,我們對每一列應用 NavigationLink。更新 List 視圖如下:
List {
ForEach(restaurants) { restaurant in
NavigationLink(destination: RestaurantDetailView(restaurant: restaurant)) {
BasicImageRow(restaurant: restaurant)
}
}
}
.listStyle(.plain)
在上列的程式碼中, 我們告訴 NavigationLink 在使用者選擇餐廳時, 導覽至 RestaurantDetailView。我們也將所選的餐廳傳送至細節視圖,以進行顯示。這就是建立導覽介面與執行資料傳送所需的全部內容。

在畫布中,你應該注意到每列資料皆已加入了一個揭露圖示。如圖 11.5 所示,你應該能夠在選擇其中一間餐廳後,導覽至細節視圖。另外,你可以點擊「返回」(Back )按鈕來導覽回內容視圖。整個導覽由 NavigationStack 自動渲染。
首先,我們來討論導覽列的顯示模式。預設情況下,導覽列是設定為顯示大標題,但當你向上滾動清單時,導覽列會變小,這是 Apple 導入「大標題」(Large Title )導覽列後的預設行為。
如果你想要使導覽列更小型,並禁用大標題,你可以在 navigationBarTitle 修飾器之下加入navigationBarTitleDisplayMode 修飾器:
.navigationBarTitleDisplayMode(.inline)
這個參數控制導覽列的外觀,不論它應顯示大標題導覽列還是小型導覽列,而預設是設定為 .automatic,即表示是使用大標題。在上列的程式碼中,我們將其設定為 .inline,即表示 iOS 使用小型導覽列,如圖 11.6 所示。

現在,我們將顯示模式改為 .automatic,看看會得到什麼,導覽列應該會再次變成大標題導覽列。
.navigationBarTitleDisplayMode(.automatic)
接下來,我們來看如何變更標題的字型與顏色。在撰寫本章時,SwiftUI 還沒有修飾器來讓開發人員設定導覽列的字型及顏色,而我們需要使用 UIKit 所提供的 UINavigation BarAppearance API。
舉例而言,我們要將導覽列的標題顏色變更為紅色、字型變更為 Arial Rounded MT Bold,則我們可以在 init() 函數中建立一個 UINavigationBarAppearance 物件,並相應地設定屬性。在 ContentView 中插入下列的函數:
init() {
let navBarAppearance = UINavigationBarAppearance()
navBarAppearance.largeTitleTextAttributes = [.foregroundColor: UIColor.red, .font: UIFont(name: "ArialRoundedMTBold", size: 35)!]
navBarAppearance.titleTextAttributes = [.foregroundColor: UIColor.red, .font: UIFont(name: "ArialRoundedMTBold", size: 20)!]
UINavigationBar.appearance().standardAppearance = navBarAppearance
UINavigationBar.appearance().scrollEdgeAppearance = navBarAppearance
UINavigationBar.appearance().compactAppearance = navBarAppearance
}
largeTitleTextAttributes 屬性用於設定大尺寸標題的文字屬性,而 titleTextAttributes 屬性則用於設定標準尺寸標題的文字屬性。當我們設定 navBarAppearance 後,將其它指定給三個外觀屬性,包括standardAppearance、scrollEdgeAppearance 與 compactAppearance。如前所述,如果需要的話,你可以為 scrollEdgeAppearance 與 compactAppearance 建立及指定一個單獨的外觀物件。

導覽視圖的「返回」(Back )按鈕預設為藍色,其使用V 形圖示(chevron icon )來表示「返回」,如圖 11.8 所示。透過使用 UINavigationBarAppearance API,你可以自訂顏色、甚至是「返回」按鈕的指示器圖片。

我們來看這個自定義是否如何工作的。要變更指示器的圖片,你可以呼叫 setBackIndicatorImage 方法,並提供自己的 UIImage。這裡,我設定系統圖片為 arrow.turn.up.left。
navBarAppearance.setBackIndicatorImage(UIImage(systemName: "arrow.turn.up.left"), transitionMaskImage: UIImage(systemName: "arrow.turn.up.left"))
對於「返回」按鈕的顏色,你可以透過設置 tint 屬性來更改它,如下所示:
NavigationStack {
.
.
.
}
.tint(.black)
如果你已經進行變更,則執行該 App 來快速測試,「返回」按鈕應該如圖 11.9 所示。

除了使用 UIKit 的 API 來自訂返回按鈕以外,另一個方式為隱藏返回按鈕,利用 SwiftUI 自己建立一個返回按鈕,要隱藏返回按鈕,如下所示,你可以使用 .navigationBarBackButtonHidden 修飾器,並將其值設定為 true:
.navigationBarBackButtonHidden(true)
SwiftUI 還提供了一個名為 toolbar 的修飾符,用於創建導航欄項目。 例如,你可以使用所選餐廳的名稱創建一個返回按鈕,如下所示:
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
dismiss()
} label: {
Text("\(Image(systemName: "chevron.left")) \(restaurant.name)")
.foregroundColor(.black)
}
}
}
在 toolbar 的閉包中,我們創建了一個 ToolbarItem 對象,其位置設置為 .navigationBarLeading。 這告訴 iOS 將按鈕放在導航欄的前沿。
要讓程式產生有效果,更新 RestaurantDetailView 如下:
struct RestaurantDetailView: View {
@Environment(\.dismiss) var dismiss
var restaurant: Restaurant
var body: some View {
VStack {
Image(restaurant.image)
.resizable()
.aspectRatio(contentMode: .fit)
Text(restaurant.name)
.font(.system(.title, design: .rounded))
.fontWeight(.black)
Spacer()
}
.navigationBarBackButtonHidden(true)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
dismiss()
} label: {
Text("\(Image(systemName: "chevron.left")) \(restaurant.name)")
.foregroundColor(.black)
}
}
}
}
}
SwiftUI 內建的環境值很廣泛。要解除目前視圖,並返回至前一個視圖。我們取得 .dismiss 環境值,然後呼叫 dismiss() 函數。請注意 .dismiss 是 iOS 15(或以上)新加入的環境值,如果你的 App 要支援比較舊的iOS 版本,你可以使用另一個環境值(即 .presentationMode):
@Environment(\.presentationMode) var presentationMode
之後,你可以利用以下程式碼呼叫presentationMode 的 dismiss() 函數:
presentationMode.wrappedValue.dismiss()
你在預覽畫布再測試 App,並選取其中一家餐廳,你會見到一個帶有餐廳名的返回按鈕。點擊返回按鈕,視圖將導覽回主畫面。
為了確認你理解如何建立導覽UI,這裡有一個作業。首先,至下列網址下載起始專案:
https://www.appcoda.com/resources/swiftui5/SwiftUINavigationStarter.zip。開啟專案後,你將看到一個顯示文章清單的範例App。
這個專案與你之前建立的專案非常類似,主要的差異是 Article.swift 的導入。這個檔案儲存了 articles 陣列,而該陣列附有一些範例資料。如果你仔細檢視 Article 結構,它現在有一個用於儲存完整文章的 content 屬性。
你的任務是將清單嵌入導覽視圖,並建立細節視圖。當使用者點擊內容視圖中其中一篇文章時,它將導覽至顯示完整文章的細節視圖,如圖 11.10 所示。我將在下一節中與你討論解決方案,但請你盡力找出自己的解決方案。

你完成作業了嗎?細節視圖比我們之前建立的視圖更複雜,我們來看看如何建立它。
為了讓程式碼更易編寫,我們將為它建立一個單獨的檔案,而不是在 ContentView.swift 檔中建立細節視圖。在專案導覽器中,右鍵點擊 SwiftUINavigation 資料夾,選擇「New File...」,接著選取「SwiftUI View」模板,並將檔案命名為「ArticleDetailView.swift」。
由於細節視圖將顯示文章的詳細資訊,我們需要這個屬性來讓呼叫者傳送文章。因此,在 ArticleDetailView 中宣告一個 article 屬性:
var article: Article
接著,更新 body 如下,以佈局細節視圖:
var body: some View {
ScrollView {
VStack(alignment: .leading) {
Image(article.image)
.resizable()
.aspectRatio(contentMode: .fit)
Group {
Text(article.title)
.font(.system(.title, design: .rounded))
.fontWeight(.black)
.lineLimit(3)
Text("By \(article.author)".uppercased())
.font(.subheadline)
.foregroundColor(.secondary)
}
.padding(.bottom, 0)
.padding(.horizontal)
Text(article.content)
.font(.body)
.padding()
.lineLimit(1000)
.multilineTextAlignment(.leading)
}
}
}
我們使用一個 ScrollView 來包裹所有的視圖,以啟用可滾動的內容。我不會逐行說明程式碼,我相信你應該了解 Text、Image 與 VStack 的運作方式,不過我想強調的修飾器是 Group,這個修飾器可以讓你將多個視圖群組在一起,並使用某個設定。在上列的程式碼中,我們需要對兩個 Text 視圖使用特定的間距設定。為了避免程式碼重複,我們將兩個視圖群組在一起,並使用間距。
現在,我們已經完成了細節視圖的佈局,但是你應該會在 Xcode 內看到一個錯誤,指出 #Preview 的問題。而預覽無法正常運作,是因為我們在 ArticleDetailView 中加入了article 屬性,因此你需要在預覽中傳送一個範例文章。更新 #Preview 來修正錯誤,如下所示:
#Preview {
ArticleDetailView(article: articles[0])
}
這裡,我們只選擇 articles 陣列中的第一篇文章來預覽。如果想要預覽其他文章,你可以將其更改為其他值。當你變更後,預覽畫布應會正確渲染細節視圖,如圖 11.11 所示。

我們再多嘗試一件事。由於這個視圖將嵌入至 NavigationView中,因此你可以修改預覽程式碼,來預覽它在預覽介面的外觀:
#Preview {
NavigationStack {
ArticleDetailView(article: articles[0])
.navigationTitle("Article")
}
}
透過更新程式後,你應該在預覽畫布中看到一個空白的導覽列。
現在我們已經完成了細節視圖的佈局,是時候該回到 ContentView.swift 來實作導覽, 更新 ContentView 結構如下:
struct ContentView: View {
var body: some View {
NavigationStack {
List(articles) { article in
NavigationLink(destination: ArticleDetailView(article: article)) {
ArticleRow(article: article)
}
.listRowSeparator(.hidden)
}
.listStyle(.plain)
.navigationTitle("Your Reading")
}
}
}
在上列的程式碼中,我們將 List 視圖嵌入至 NavigationStack 中, 並對每一列應用 NavigationLink。導覽連結的目的地設定為我們剛才建立的細節視圖。在預覽中,你應該可透過點擊「播放」(Play )按鈕來測試 App,並在選擇文章後,導覽至細節視圖。
這個 App 運作得很完美,但是有兩個問題你可能想要微調。首先是內容視圖中的揭示指示器(disclosure indicator ),這裡顯示揭示指示器有點奇怪,我們可以禁用它嗎?第二個問題是,在細節視圖中精選圖片的上方出現空白區域。我們來逐一討論這些問題。

SwiftUI 並沒有為開發者提供禁用或隱藏揭示指示器的選項。為了解決這個問題,我們不直接將 NavigationLink 應用於文章列,而是建立一個具有兩層的 ZStack。現在更新 ContentView 的 NavigationView 如下:
NavigationStack {
List(articles) { article in
ZStack {
ArticleRow(article: article)
NavigationLink(destination: ArticleDetailView(article: article)) {
EmptyView()
}
.opacity(0)
.listRowSeparator(.hidden)
}
}
.listStyle(.plain)
.navigationTitle("Your Reading")
}
下層是文章列,上層則是空視圖。NavigationLink 現在應用於空視圖,以避免 iOS 渲染揭示按鈕。當你變更後,揭示指示器就會消失,但你仍然可以導覽至細節視圖。
現在,我們來看第二個問題的根本原因。
切換到 ArticleDetailView.swift,在設計細節視圖時,我沒有提到這個問題,但實際上從預覽中,你應該會發現這個問題,如圖 11.13 所示。

圖片上方會出現空白區域的原因是導覽列的緣故。這個空白區域實際上是一個帶有空白標題的大尺寸導覽列,當App 從內容視圖導覽至細節視圖時,導覽列會變成標準尺寸列。因此,要修復這個問題,我們需要做的是明確指定使用標準尺寸導覽列。
在 ScrollView 的括號後,插入下列這行程式碼:
.navigationBarTitleDisplayMode(.inline)
透過將導覽列設定為 inline 模式後,空白區域將被最小化,現在你可回到 ContentView.swift 來再次測試App,細節視圖現在看起來好多了。
雖然你可使用內建的屬性來自訂「返回」按鈕指示器圖片,有時你可能想要建立一個客製化「返回」按鈕來導覽回內容視圖。問題是如何透過編寫程式碼來完成呢?
在最後一個小節中,我要介紹如何透過隱藏導覽列及建立自己的「返回」按鈕,來建立一個更精緻的細節視圖。首先,我們看一下如圖 11.14 所示的最終設計,看起來不錯吧?

要佈局這個畫面,我們必須要解決兩個問題:
iOS 有一個「安全區域」(safe area )的觀念,用於輔助視圖的佈局。安全區域可幫你將視圖放置於介面的可見部分,例如:安全區域防止視圖隱藏了狀態列。若是你的 UI 導入了導覽列,則會遮擋導覽列。
](images/navigation/swiftui-navigation-15.jpg)
要放置超出安全區域的內容,你可以使用名為 ignoresSafeArea 修飾器。對於我們的專案,由於我們想要滾動視圖超出安全區域的頂部邊緣,則可編寫修飾器如下:
.ignoresSafeArea(.all, edges: .top)
這個修飾器接收其他值,如 .bottom 與 .leading。如果你想要忽略整個安全區域,則可以直接使用.ignoresSafeArea()。透過將這個修飾器加到 ScrollView,我們可以隱藏導覽列,並實現一個視覺上賞心悅目的細節視圖。

現在談到關於建立自己的「返回」按鈕的第二個問題,這個問題比第一個問題更棘手。下面是我們要實作的內容:
為了隱藏「返回」按鈕,SwiftUI 提供一個名為 navigationBarBackButtonHidden 的修飾器。你只需將其值設定為 true,即可隱藏「返回」按鈕:
.navigationBarBackButtonHidden(true)
當隱藏「返回」按鈕後,你可以使用自己的按鈕來替代它。toolbar 修飾器允許你配置導航欄項目。 在閉包中,我們使用ToolbarItem自訂後退按鈕,並將該按鈕指定為導航欄的左按鈕。 以下是相關程式碼:
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
// 導覽至前一個畫面
}) {
Image(systemName: "chevron.left.circle.fill")
.font(.largeTitle)
}
.tint(.white)
}
}
你可以將上述的修飾器加到 ScrollView。當修改生效後,你應該會在預覽畫布中看到我們自己的客製化「返回」按鈕,如圖 11.17 所示。

你可能發現按鈕的 action 閉包被留空。「返回」按鈕已經佈局得不錯了,但問題是它不能運作。
原來的「返回」按鈕是由 NavigationView 渲染,可以自動導覽回前一個畫面。問題來了,我們該如何編寫程式碼來觸發這個操作呢?感謝 SwiftUI 框架所內建的環境值(environment value ),你可以引用一個名為dismiss 環境綁定(environment binding ),來導覽至前一個畫面。
現在,在 ArticleDetailView 宣告一個 dismiss 變數來取得環境值:
@Environment(\.dismiss) var dismiss
接下來,在我們的客製化「返回」按鈕的 action 中,插入下列這行程式碼:
dismiss()
這裡,我們呼叫 dismiss 方法,以在點擊「返回」按鈕時解除細節視圖。現在,你可以執行 App 並再次測試它,你應該能夠在內容視圖與細節視圖之間進行導覽。
導覽 UI 在行動 App 中非常常見,理解我在本章所介紹的內容非常重要。如果你完全理解了內容,即使資料是靜態的,你也可以建立一個基於內容的簡單 App。
在本章所準備的範例檔中,有完整的專案可供下載:
要進一步學習導覽視圖,你也可以參考下列 Apple 所提供的文件: