教面試官ReentrantLock源碼
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
注意:該方法在 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 中屬于全局配置,即配置一次后,對運行時間接下來的所有標題欄也生效,這個全局配置是通過 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
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小時內刪除侵權內容。