在 iOS 開發中,導覽視圖 (Navigation View) 絕對是我們最常用的組件。在 SwiftUI 剛推出的時侯,就已經有一個 NavigationView
視圖,讓開發者可以構建基於導覽的使用者界面。隨著 iOS 16 的發佈,Apple 棄用了舊的導覽視圖,並引入了一個新視圖 NavigationStack
來呈現堆疊視圖。更重要的是,開發者現在可以利用這個新視圖來構建資料導向 (data driven) 的導航。
舊的 Navigation Views 的操作
在 iOS 16 之前,我們會用 NavigationView
和 NavigationLink
如此構建導覽介面:
NavigationView {
NavigationLink {
Text("Destination")
} label: {
Text("Tap me")
}
}
以上程式碼會構建出一個基本的導覽介面,當中有一個 Tap me 按鈕。當我們點擊按鈕,App 就會導覽到下一級,來顯示目標視圖。
使用 NavigationStack
從 iOS 16 開始,我們可以用新的 NavigationStack
來取代 NavigationView
。我們可以完全保留 NavigationLink
,都會得到相同的結果。
NavigationStack {
NavigationLink {
Text("Destination")
} label: {
Text("Tap me")
}
}
我們也可以這樣編寫程式碼:
NavigationStack {
NavigationLink("Tap me") {
Text("Destination")
}
}
要顯示一個資料項目的列表,通常我們會使用導覽視圖來構建一個 master-detail flow。來看看以下的範例:
struct ContentView: View {
private var bgColors: [Color] = [ .indigo, .yellow, .green, .orange, .brown ]
var body: some View {
NavigationStack {
List(bgColors, id: \.self) { bgColor in
NavigationLink {
bgColor
.frame(maxWidth: .infinity, maxHeight: .infinity)
} label: {
Text(bgColor.description)
}
}
.listStyle(.plain)
.navigationTitle("Color")
}
}
}
以上的程式碼會建立一個導覽視圖,來顯示構建一個 master-detail flow。當使用者選擇了一個項目,App 就會導覽到 detail 視圖,並顯示 color 視圖。
建基於數值的 Navigation Links
NavigationStack
引入了一個新修飾符 navigationDestination
,用來把目標視圖與呈現的資料型別連繫。我們可以這樣重新編寫上一節的程式碼:
NavigationStack {
List(bgColors, id: \.self) { bgColor in
NavigationLink(value: bgColor) {
Text(bgColor.description)
}
}
.listStyle(.plain)
.navigationDestination(for: Color.self) { color in
color
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.navigationTitle("Color")
}
我們還是會使用 NavigationLinks
來顯示資料列表,並實作導覽功能;不同之處是每個 NavigationLink
都連繫著一個數值。更重要的是,我們添加了新的 navigationDestination
修飾符來捕捉數值的變化。當使用者選擇特定的 link 時,navigationDestination
修飾符就會顯示相應帶有 Color
型別資料的目標視圖。
如果我們在預覽中測試這個 App,你會發現它的操作方式與之前完全相同。但是,內部的實作已經使用了新的 navigationDestination
修飾符。
多個 Navigation Destination 修飾符
我們可以定義多於一個 navigationDestination
修飾符,來處理不同型別的 Navigation Link。在之前的範例中,我們只有一個 navigationDestination
修飾符來處理 Color
型別。現在,讓我們為 String
型別設置另一組 Navigation Link:
List(systemImages, id: \.self) { systemImage in
NavigationLink(value: systemImage) {
Text(systemImage.description)
}
}
.listStyle(.plain)
systemImages
變數會儲存系統圖像名稱的陣列 (array)。
private var systemImages: [String] = [ "trash", "cloud", "bolt" ]
在這個範例中,我們有兩個型別的 Navigation Link,一個是 Color
,另一個是 String
型別。讓我們把另一個 navigationDestination
修飾符嵌入到堆疊中,來處理 String
型別的導覽:
.navigationDestination(for: String.self) { systemImage in
Image(systemName: systemImage)
.font(.system(size: 100.0))
}
現在,如果使用者點擊其中一個系統圖像名稱,就會導覽到另一個視圖,來顯示系統圖像。
Working with Navigation States
與舊的 NavigationView
不同,新的 NavigationStack
讓我們可以輕鬆地追踪導覽的狀態。NavigationStack
視圖有另一個 initialization 方法,它有一個 path
參數 (parameter),就是堆疊導覽狀態的 binding:
init(
path: Binding<Data>,
root: () -> Root
) where Data : MutableCollection, Data : RandomAccessCollection, Data : RangeReplaceableCollection, Data.Element : Hashable
如果我們想儲存或管理導覽狀態,可以創建一個狀態變數。以下是範例程式碼:
struct ContentView: View {
private var bgColors: [Color] = [ .indigo, .yellow, .green, .orange, .brown ]
@State private var path: [Color] = []
var body: some View {
NavigationStack(path: $path) {
List(bgColors, id: \.self) { bgColor in
NavigationLink(value: bgColor) {
Text(bgColor.description)
}
}
.listStyle(.plain)
.navigationDestination(for: Color.self) { color in
VStack {
Text("\(path.count), \(path.description)")
.font(.headline)
HStack {
ForEach(path, id: \.self) { color in
color
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
List(bgColors, id: \.self) { bgColor in
NavigationLink(value: bgColor) {
Text(bgColor.description)
}
}
.listStyle(.plain)
}
}
.navigationTitle("Color")
}
}
}
這段程式碼與之前的範例有點相似,我們添加了一個 path
狀態變數,它是 Color
的陣列,用來儲存導覽狀態。在 NavigationStack
進行 initialization 時,我們會傳遞它的 binding 來管理堆疊。當導覽堆疊的狀態發生變化時,path
變數的數值就會被自動更新。
我更改了導覽目的地,它會顯示使用者選擇的顏色,和另一個顏色列表以供使用者選擇。
在上面的程式碼中,我們有一行程式碼來顯示 path 的內容:
Text("\(path.count), \(path.description)")
count
屬性 (property) 是指堆疊的 level,而 description 就是當前的顏色。舉個例子,我們首先選擇顏色 indigo,然後選擇 yellow。在這個情況下,count
的數值就應該是 2,代表導覽堆疊有 2 個 level。
我們可以利用這個 path
變數,來以編程方式控制堆疊的導覽。舉個例子,我們可以添加一個按鈕,讓使用者直接跳轉到堆疊的 root level,以下是範例程式碼:
Button {
path = .init()
} label: {
Text("Back to Main")
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
我們可以重置 path
變數的數值,來指示導覽堆疊返回 root level。
大家可能已經知道,我們可以操縱 path
變數的數值,來控制導覽堆疊的狀態。舉個例子,我們可以向 path
變數添加三種顏色,如此一來,當 ContentView
出現時,App 就會自動導覽 3 個 level:
NavigationStack(path: $path) {
.
.
.
}
.onAppear {
path.append(.indigo)
path.append(.yellow)
path.append(.green)
}
你可以試著執行 App,就會看到 App 自動導覽了 3 個 level。如此一來,我們就可以以編程方式控制導覽狀態,並處理 deep linking。
總結
iOS 16 推出的新 NavigationStack
,可以讓開發者輕鬆地構建資料導向的導航 UI。如果你的 App 不需要支援舊版本的 iOS,就可以利用這個新元件,來處理 deep linking 和複雜的 user flow。