自推出以來,NavigationView 一直都是 SwiftUI Navigation 框架的致命弱點。它之前不支援 NavigationLink 中延遲載入目標視圖(雖然後來解決了這個問題)、以及無法以編程方式導航 Deep Link 等問題,總是讓我們逼不得已改用 UINavigationController
。
幸好,在 iOS 16 中,Apple 推出了一個以數據驅動的新導航結構,與之前以視圖驅動的結構不同。
新的 Navigation API 有幾個重要的改善,包括一個 NavigationStack
,讓開發者可以從堆疊 (stack) 中推送和彈出視圖,一個 NavigationPath
,用於管理 routing 堆疊,以及一個修飾符 navigationDestination
,用來以編程方式有效率地導航視圖。而在這次更新中,Apple 也棄用了 NavigationView
。
簡單介紹 NavigationStack
要在視圖層次結構 (hierarchy) 堆疊中匯入 NavigationStack,步驟非常簡單:
NavigationStack {
NavigationLink {
Text("Destination Screen")
} label: {
Text("Goto Next Screen")
}
}
在一般情況下,我們可以利用新的 NavigationStack
容器 (container) 直接重構舊的 NavigationView
。不過,讓我們看看新 NavigationLink
的 init 語法。
現在的 API 有一個 value
-label
語法,label 承載連結的內容視圖,而 value 則包含目標視圖的 builder。
另一個值得注意的地方,是 NavigationLink
的舊 init 方法如 NavigationLink(isActive:destination:label:)
已被棄用。
同樣地,在 iOS 16 中,NavigationLink(destination:tag:selection:)
也被棄用了。也就是說,我們需要完全不同的程式碼,來重構建基於 NavigationLink 程序化導航。
利用 navigationDestination 修飾符來程序化導航
在前文中,我們看到了如何在 init 程式碼中設置 NavigationLink
的目標視圖。但是,大家還記得在 iOS 16 之前,要構建複雜的導航結構時,isActive
boolean flag 為我們帶來多少痛苦嗎?
幸好,從 iOS 16 開始,我們可以在 .navigationDestination
修飾符中設置目標視圖。有了 navigationDestination
,我們就可以根據類型 (type),以編程方式 route 到不同的畫面。我們還可以為不同視圖類型,添加多個 navigationDestination
修飾符。
在以下的範例中,我們會構建一個 SwiftUI 導航 App,這個 App 會在目標畫面上構建同樣的列表視圖:
NavigationStack {
RowListsView()
.navigationDestination(for: Int.self) { i in
RowListsView()
}
}
利用 NavigationPath 來 Route NavigationLink
之前,我們會利用 tags
來 route NavigationLink。
在 iOS 16 之後,我們有新的 NavigationPath
,來保存與在 NavigationStack
中顯示的視圖有關的類型擦除 (type erased) 數據。
NavigationPath
的強大之處,在於它能夠輕鬆地從堆疊中推送或彈出屬於不同數據類型的視圖。
讓我們看看以下的範例 App,我們在 NavigationStack
中插入了 NavigationPath
:
final class Router: ObservableObject {
@Published var path = NavigationPath()
}
struct ContentView: View {
@EnvironmentObject var router: Router
var body: some View {
NavigationStack(path: $router.path) {
RowListsView()
.navigationDestination(for: Int.self) { i in
RowListsView()
}
}
.environmentObject(router)
}
}
我們把 NavigationPath
設置在 ObservableObject
類別 (class) 中,並把它設置在 EnvironmentObject
中,以便將其傳遞給子視圖。不過,我們也可以使用 @Binding
來達到同樣的效果。
你可以留意一下修改後的 NavigationStack(path:)
init。
以下是 RowListsView
的程式碼:
struct RowListsView : View{
@EnvironmentObject var router: Router
var body: some View{
Form{
List(1..<5) { i in
NavigationLink(value: i) {
Text("\(i)")
}
}
Section{
if router.path.count > 0
{
Button("Screen count \(router.path.count)"){
print("")
}
Button("Pop to root", role: .destructive){
router.path = .init()
}
Button("Jump back two screens", role: .none){
if router.path.count >= 2{
router.path.removeLast(2)
}
else if router.path.count >= 1{
router.path.removeLast(1)
}
}
}
}
}
}
}
我們稍微修改了上面的 SwiftUI 視圖,加入了客製化的 back 按鈕,這些按鈕會以編程方式改變 NavigationPath
,最後改變了視圖 NavigationStack
:
我們可以看到,只要調用以下程式碼,就可以簡單地彈回 root:
router.path = .init()
我們可以長按 back 按鈕,來顯示不同畫面標題的下拉選單,讓我們可能選擇彈回任何畫面。 現在,標題會預設顯示為 back
,但我們可以設置 .navigationTitle(string:)
修飾符來客製化標題。
總結
以上就是新的 SwiftUI NavigationStack 的簡介,我們可以利用新模式做很多事情,像是處理 deep link 等。
此外,你也可以參考這個開源程式庫,來把新的 Navigation API backport 到之前的 iOS 版本。
這篇文章到此為止。你可以在 GitHub 上參閱完整的 SwiftUI 程式碼。