SwiftUI之NavigationView的基礎使用與進階實踐

      網友投稿 1204 2025-04-02

      一、前言


      在 UIKit 的框架中,我們時常使用 UINavigationViewController 來管理頁面的 push 和 pop,這是頁面管理的基本操作。而到了 SwiftUI,該操作是交由 NavigationView 和 NavigationLink 來完成。

      本文先從 NavigationView 的基本應用開始,再補充如何靈活的使用 NavigationView 來完成很多更細節化的需求。

      二、基本概念

      如下所示,用一個 demo 展示了 NavigationView 和 NavigationLink 的基本應用:

      // NavigationView基礎 import SwiftUI @main struct iOS_testApp: App { var body: some Scene { WindowGroup { NavigationView { NavigationLink( destination: Text("Destination"), label: { Text("Navigate") }) } } } }

      在該示例中,提供了一個頂層 View,即 NavigationView,在 SwiftUI 中,NavigationView 相當于 UIKit 的 UINavigationViewController,它提供了整個頁面導航環境的頂層容器,包裹在 NavigationView 下面的是 NavigationLink,它定義了本頁面的視圖以及待 push 的視圖(通過點擊)。

      如在示例中,Text(“Navigate”) 就是本頁面的視圖,而 Text(“Destination”) 就是點擊跳轉后的視圖。主界面如下所示,點擊 Navigate 即可 push:

      點擊 Navigate 后 push 新界面 Destination:

      三、設置標題欄

      在 NavigationView 的默認展示設置中,根級界面是沒有標題欄的,而待 push 的界面默認帶標題返回欄,但是標題為空。通過 .navigationBarTitle 修飾屬性可以對標題進行設置:

      // NavigationView根界面帶標題欄 import SwiftUI @main struct iOS_testApp: App { var body: some Scene { WindowGroup { NavigationView { NavigationLink( destination: Text("Destination"), label: { Text("Navigate") }) .navigationBarTitle("Main", displayMode: .large) } } } }

      帶 large 標題欄的 Navigate 界面,如下所示:

      其中 displayMode 是一個枚舉類型參數,支持 inline,large 和 automatic,分別表示小標題欄,大標題欄和自動選擇,如果你選擇 automatic,則一般系統會選擇 large。

      四、隱藏標題欄

      某些情況下,如果不希望使用標題欄,或者不喜歡 NavigationView 提供的標題欄樣式,對它提供的定制靈活性并不滿意,而希望完全由自己接管和實現標題欄,在這種情況下,可以選擇隱藏標題欄,隱藏標題欄通過 .navigationBarHidden(true) 來完成:

      // 隱藏destination標題欄 import SwiftUI @main struct iOS_testApp: App { var body: some Scene { WindowGroup { NavigationView { NavigationLink( destination: Text("Destination") // 隱藏二級界面的標題欄 .navigationBarHidden(true), label: { Text("Navigate") }) .navigationBarTitle("Main", displayMode: .automatic) } } } }

      隱藏了標題欄的 Destination 界面,如下所示:

      五、編程實現頁面返回邏輯

      當隱藏了二級界面的標題欄后,我們豈不是把標題欄的返回按鈕也隱藏了,那么要實現自己的返回按鈕時,該怎么做呢?這時候就需要用到 SwiftUI 獨有的機制:視圖環境 @Environment,Environment 提供了視圖共享的屬性綁定服務,通過這些屬性可以完成視圖的基本操作,其中一個屬性叫 presentationMode,該屬性綁定了導航頁面間的上下文關系,通過它的 dismiss 方法可以手動返回頁面:

      // 通過編程實現頁面返回邏輯 import SwiftUI struct DestinationView: View { // 聲明屬性presentationMode @Environment(\.presentationMode) var presentationMode: Binding var body: some View { Text("Destination View") .navigationBarHidden(true) // 追加后destination不再出現標題欄 .onTapGesture { // 點擊"Destination View"后返回 self.presentationMode.wrappedValue.dismiss() } } } struct ContentView: View { var body: some View { NavigationView { NavigationLink( destination: DestinationView(), label: { Text("Navigate View") }) } } } @main struct iOS_testApp: App { var body: some Scene { WindowGroup { ContentView() } } }

      注意:該方法在 iOS 15.0 后即將被屬性 dismiss 替代,但是考慮到撰寫本文時主流系統是 iOS 14.5,出于兼容需要,依然使用 presentationMode 來完成代碼。

      在以上例子中,把 Text(“Destination”) 這個二級界面單獨提取到 DestinationView 中, 也單獨提出 ContentView。通過聲明 @Environment(.presentationMode),讓 DestinationView 獲取了 presentationMode 屬性的綁定數據。

      接下來給 Text(“Destination View”) 提供點擊操作: onTapGesture,在點擊的實現代碼里調用 self.presentationMode.wrappedValue.dismiss() 。

      運行程序,現在可以通過點擊 Navigate View 和 Destination View 自由往返。

      六、標題欄樣式設置

      現在知道 NavigationView 提供了導航的基本元素,并且提供了系統默認的標題欄,我們可以隱藏標題欄從而自行設計界面。那么當我們想用默認的標題欄,但是想要改變其中的某些樣式,比如標題顏色,應該怎么做呢?

      SwiftUI之NavigationView的基礎使用與進階實踐

      事實上,更改標題欄的樣式在 SwiftUI 中屬于全局配置,即配置一次后,對運行時間接下來的所有標題欄也生效,這個全局配置是通過 UINavigationBar.appearance() 來實現的。

      修改 ContentView 如下:

      // 設置標題欄標題為紅色 struct ContentView: View { var body: some View { NavigationView { NavigationLink( destination: DestinationView(), label: { Text("Navigate View") }) .navigationBarTitle("Title", displayMode: .inline) .onAppear() { // 設置標題顏色 UINavigationBar.appearance().titleTextAttributes = [.foregroundColor: UIColor.red] } } } }

      如下所示,Navigate View 標題欄的標題為紅色,樣式為 inline:

      運行應用程序,可以發現 title 是紅色的,與此同時,該設置對 DestinationView 也同樣有效:

      // 展示DestinationView標題,一樣發現是紅色 struct DestinationView: View { @Environment(\.presentationMode) var presentationMode: Binding var body: some View { Text("Destination View") .navigationBarHidden(false) // 標題欄不隱藏 .navigationTitle("Title 2") // 追加標題 .onTapGesture { self.presentationMode.wrappedValue.dismiss() } } }

      Destination View 標題欄設置為紅色:

      七、進階:去掉點擊的交互特效

      當運行示例程序時,很容易會發現點擊 Navigate View 會出現一個明顯的漸變特效,另一方面,也很容易發現 Navigate View 的字體顏色是經典的 iOS 7 藍,這是默認的按鈕效果,對于這種效果有些人覺得很好,但是對于開發者開發的應用,由于界面風格的不同,該特效并不是什么時候都是合適的。如果想移除這個效果,該怎么做?

      Navigate View 被按下時的默認展示效果,如下:

      這時候就要用到 buttonStyle 修飾器,在 SwiftUI 中,它的完整聲明如下:

      // buttonStyle的聲明(不是我寫的) extension View { public func buttonStyle(_ style: S) -> some View where S : ButtonStyle }

      通過該修飾器來完成 Button 樣式的修改,而傳入的參數 ButtonStyle 由自己定義。也就是說,在此之前需要定義一個 ButtonStyle 的 Struct,代碼如下:

      // 定義一個ButtonStyle,命名為DefaultButtonStyle struct DefaultButtonStyle: ButtonStyle { func makeBody(configuration: Self.Configuration) -> some View { configuration.label .background(configuration.isPressed ? Color.clear : Color.clear) } }

      在本例中,把背景顏色全部改成了.clear,開發者可以根據自身需求修改。并且 configuration的isPressed 狀態屬性也很有用,可以根據狀態改變按鈕視覺。接下來在 ContentView 中設置 buttonStyle:

      // 設置buttonStyle struct ContentView: View { var body: some View { NavigationView { NavigationLink( destination: DestinationView(), label: { Text("Navigate View") }) .navigationBarTitle("Title", displayMode: .inline) // 設置按鈕樣式 .buttonStyle(DefaultButtonStyle()) .onAppear() { UINavigationBar.appearance().titleTextAttributes = [.foregroundColor: UIColor.red] } } } }

      運行應用程序,就會發現按鈕樣式已經不再有原先的樣式特效,消除了默認特效后的"Navigate View"如下所示:

      八、進階:支持默認點擊之外的更多交互

      到目前為止,NavigationView 和 NavigationLink 已經可以滿足我們日常開發的大部分需求了。但是,在某些情況下,我們對產品的交互有更豐富的需求。例如,在本例中,NavigationLink 默認支持點擊操作,但是如果我們想要更多的操作響應怎么辦,比如長按響應?

      開始進行嘗試,先把 ContentView 進行簡化,去掉原先追加的若干代碼,然后加入 onLongPressGesture:

      // 嘗試加入onLongPressGesture struct ContentView: View { var body: some View { NavigationView { NavigationLink( destination: DestinationView(), label: { Text("Navigate View") .onLongPressGesture { print("long press") } }) } } }

      運行代碼,可以發現,當長按 Navigate View 時,確實打印出了"long press",但是同時 NavigationLink 的點擊響應也失效了,這明顯不符合我們需求。原因在于支持了 onLongPressGesture,NavigationLink 的按鈕屬性也被更高優先級的 gesture 取代,按鈕點擊功能不再有效。

      如何既支持長按,又支持點擊呢?這里提供的一個方案是,加入 onTap 操作:

      // 通過onTapGesture來支持點擊響應 struct ContentView: View { var body: some View { NavigationView { NavigationLink( destination: DestinationView(), label: { Text("Navigate View") .onTapGesture { print("tap") } .onLongPressGesture { print("long press") } }) } } }

      再次運行代碼,這時候可以發現"tap"和"long press"都可以正確打印。

      九、isActive 參數

      在之前的代碼中,已經可以用 onTapGesture 和 onLongPressGesture 來分別響應 NavigationLink 的交互,但是也發現了一個問題。NavigationLink 最重要的跳轉問題,還沒有得到解決。現在引進一個重要參數隆重登場:isActive,它是 NavigationLink 構造函數的一個參數,默認值為 .constant(true),先來看看它的正確使用方法:

      // 引入了isActive來手動跳轉 struct ContentView: View { @State private var isActive = false // 定義isActive狀態,默認為false var body: some View { NavigationView { NavigationLink( destination: DestinationView(), isActive: $isActive, // 綁定isActive label: { Text("Navigate View") .onTapGesture { print("tap") isActive = true // 點擊的時候,設置為true觸發跳轉 } .onLongPressGesture { print("long press") } }) } } }

      isActive 是 NavigationLink 插入二級頁面的觸發參數,如果它是個常量,為 false 時則不會觸發,為 true 時則在點擊的時候觸發。但是如果參數是一個 @State 變量,則是由 @State 的變量值來決定是否插入二級頁面。

      在以上代碼里,定義了名為 isActive 的 @State 變量,初始值為 false,并且將它綁定到 NavigationLink的isActive 參數中,當用戶點擊"Navigate View"時,觸發 onTapGesture,在其實現代碼中,設置 isActive 為 true,成功觸發 destination 的載入操作,這時候如預期的加載 DestinationView。

      十、更復雜案例:多個 NavigationLink 下的情況

      在實戰項目中,還會遇到更多關于 NavigationView 的挑戰,但是方法總比問題多,總有應對之策。剛剛提到一個 @State 變量 isActive 可以解決由編程決定載入頁面的問題。但是在項目實踐中,往往有多個 NavigationLink,它們或者由 VStack 組成,或者由 ScrollView 組成。在這種情況下,一個 isActive 變量完全不夠用。一個不夠,就出動多個,用一個數組來解決問題。

      現在用一個完整的程序代碼來展示下用法:

      // 通過數組控制頁面的導航 import SwiftUI struct ContentView: View { // 用數組替代單一的變量 @State private var isActives: [Bool] = Array(repeating: false, count: 2) var body: some View { NavigationView { VStack { NavigationLink( destination: Text("Destination View 1"), isActive: $isActives[0], label: { Text("Navigate View 1") .onTapGesture { print("tap 1") isActives[0] = true } .onLongPressGesture { print("long press 1") } }) NavigationLink( destination: Text("Destination View 2"), isActive: $isActives[1], label: { Text("Navigate View 2") .onTapGesture { print("tap 2") isActives[1] = true } .onLongPressGesture { print("long press 2") } }) } } } } @main struct iOS_testApp: App { var body: some Scene { WindowGroup { ContentView() } } }

      運行程序,可以看到"Destination View 1"和"Destination View 2"都可以很好的響應點擊、長按等交互:

      十一、進階技巧:NavigationLink 數目可變條件下的編程

      通過 isActives 數組控制 NavigationLink 跳轉的思路雖然是對的,但是示例代碼并不能解決實際項目中的需求。因為在樣例中 isActives 數組數目是已知的固定的,而在實際項目中,NavigationLink 數目可能是動態下發的,這種情況下該如何編碼呢?

      下面來看看,一個典型的根據數組元素構建的 NavigationLink 是如何編寫的:

      // 由數組items決定NavigationLink數量 struct ContentView: View { @State private var items: [Int] = [] var body: some View { NavigationView { ScrollView { ForEach(items, id: \.self) { item in NavigationLink( destination: Text("Destination View \(item)"), label: { Text("Navigate View \(item)") }) } } } .onAppear() { items = Array(arrayLiteral: 1, 2) } } }

      在以上代碼中,NavigationLink 由 items 動態決定,而不是一段一段的寫死,通過 ForEach 來逐個創建 NavigationLink,那么問題來了:如果在這種情況下實現原先的點擊/長按需求,該怎么做?

      解決方法有很多,在這里提供一個我的解決方案,代碼如下:

      // 動態的isActives數組完成狀態綁定 struct ContentView: View { @State private var isActives: [Bool] = [] @State private var items: [Int] = [] var body: some View { NavigationView { ScrollView { ForEach(items, id: \.self) { item in NavigationLink( destination: Text("Destination View \(item)"), isActive: $isActives[self.items.firstIndex(of: item)!], // 正確的綁定item所對應的isActive數組位置 label: { Text("Navigate View \(item)") .onTapGesture { print("tap \(item)") isActives[self.items.firstIndex(of: item)!] = true // 點擊的時候,獲取正確的數組下標并修改綁定值 } .onLongPressGesture { print("long press \(item)") } } ) } } } .onAppear() { items = Array(arrayLiteral: 1, 2) isActives = Array(repeating: false, count: items.count) // 動態創建isActives數組,和items數目保持一致 } } }

      以上代碼實現了用數組 isActives 動態綁定了每一個 NavigationLink 的 isActive 屬性。在以上實現過程中,需要注意:

      通過在 onAppear 下的代碼,動態創建 isActives 數組,數組的個數和 items 數目保持一致:

      isActives = Array(repeating: false, count: items.count)

      在 ScrollView 循環創建 NavigationLink 的 ForEach 中,通過以下方式獲得正確的下標:

      self.items.firstIndex(of: item)!

      把 $isActives[self.items.firstIndex(of: item)!] 綁定到 isActive 參數中;

      在點擊事件中,將綁定的數組元素設置為 true;

      isActives[self.items.firstIndex(of: item)!] = true

      iOS 容器

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:Excel模擬運算表
      下一篇:辦公表格軟件剪切圖片(辦公表格軟件剪切圖片怎么操作)
      相關文章
      国产精品亚洲一区二区三区在线| 久久精品夜色噜噜亚洲A∨| 亚洲午夜成人精品电影在线观看| 亚洲国产成人精品无码区花野真一 | 久久精品国产亚洲AV嫖农村妇女 | 亚洲成AV人片在WWW| 亚洲精品无码专区久久| 亚洲欧美aⅴ在线资源| 亚洲精品国产摄像头| 午夜亚洲WWW湿好爽| 无码欧精品亚洲日韩一区夜夜嗨 | 亚洲国产成人五月综合网 | 亚洲综合无码精品一区二区三区| 国产亚洲精品久久久久秋霞 | 亚洲一区AV无码少妇电影| 亚洲字幕AV一区二区三区四区 | 国产AV无码专区亚洲AV麻豆丫| 国产成人人综合亚洲欧美丁香花| 国产成人高清亚洲一区久久 | 国产午夜亚洲精品午夜鲁丝片| 亚洲无av在线中文字幕| 久久亚洲国产伦理| 中文字幕亚洲精品| 亚洲三级视频在线观看| 中文日韩亚洲欧美制服| 日韩欧美亚洲国产精品字幕久久久 | 亚洲免费在线视频观看| 亚洲午夜福利在线视频| 国产亚洲精品美女2020久久| 亚洲日韩在线中文字幕第一页| 亚洲色精品vr一区二区三区| 亚洲AV日韩AV永久无码下载| 亚洲日本视频在线观看| 亚洲一本一道一区二区三区| 亚洲av成人一区二区三区在线观看| 相泽亚洲一区中文字幕| 亚洲国产精品人久久| 激情综合亚洲色婷婷五月| 亚洲av日韩精品久久久久久a| 亚洲乱码中文字幕手机在线| 国产亚洲精品a在线无码|