去年看到過美團點評技術團隊的一篇文章iOS系統中導航欄的轉場解決方案與最佳實踐,文章對系統導航欄的改造很有意思,最近就試著寫點程式碼練練手。
專案地址:DoubleNavigationController
這個庫還沒有在實際專案中檢驗過,還有很多不完善或者不能滿足業務需求的地方,歡迎提issue或者PR。
些許疑問
- 為什麼要開發這個庫?
UINavigationController
在蘋果官方文件上的是這樣介紹的:
A navigation controller is a container view controller that manages one or more child view controllers in a navigation interface.
也就是說UINavigationController
是作為UIViewController
的管理者,因此它的NavigationBar不應該從屬於任何一個ViewController。但是大部分UI設計者都沒有明白蘋果設計的用意,因此在業務中經常出現一個場景:在邏輯上應該從屬於同一個NavigationController的多個ViewController卻擁有不同的NavigationBar樣式。
不同頁面的開發者只能看到對自己開發的便利性,對UINavigationController
的理解不到位,到處修改NavigationBar樣式,在頁面轉場過程中NavigationBar出現了各種不可控的問題。這個問題在App支援路由後會變得更為突出,原因是各個頁面的跳轉關係將會非常複雜且不可預知。
DoubleNavigationController解決的便是這個問題,讓開發者自由地修改NavigationBar樣式,並且不用擔心在退回到棧下ViewController後NavigationBar的樣式也被修改。簡單來說就是,我們修改NavigationBar不再會影響棧內現有的頁面樣式,而隻影響之後Push的新頁面。
- 為什麼不設計成既不影響棧內現有的頁面樣式,也不影響新頁面?
在這裡先講一下DoubleNavigationController的兩個設計思想 “先到先得”、“誰用誰修改”。
“先到先得”
先出現的頁面樣式不應該受到後出現的頁面影響。使用者在使用過程中先看到了頁面A的樣式,接著從A頁面跳轉到B頁面,B頁面的導航欄樣式與A頁面不同,這時使用者再返回A頁面,從正常邏輯上來說,使用者希望看到的A頁面導航欄應該還是之前見過的樣式,不應該受到B的影響而改變。
“誰用誰修改”
繼續上面一個場景,使用者從頁面A到頁面B,再從頁面B跳轉到頁面C,在上一個場景下我們知道,頁面B修改了導航欄樣式,使其與頁面A不同,當我們跳轉到頁面C時,此時存在如下兩種可能:
-
頁面C也對剛才B頁面修改過的導航欄樣式屬性進行了修改。
-
頁面C沒有對剛才B頁面修改過的導航欄樣式屬性進行修改。
在第1種情況下我們很容易確定,C頁面的導航欄樣式就應該是C頁面自己修改的樣式。那麼在第2種情況下,C頁面導航欄應該長什麼樣?
再考慮以下3種方案:跟A頁面一樣?跟UIAppearance配置一樣?跟B頁面一樣?
C頁面導航欄跟A頁面一樣?
這個方案在邏輯上就是錯誤的,因為C頁面根本不應該關心它的上上一個頁面樣式。如下圖,假設B頁面有兩條跳轉路徑A1和A2,此時C頁面的樣式就有2種可能,相信絕大多數App的設計都不會出現 “1個頁面,2種UI” 的情況吧。
C頁面導航欄跟UIAppearance配置一樣?
讓C頁面保持和UIAppearance配置一致,這裡也存在兩個問題,一個問題是如果使用者沒有配置UIAppearance怎麼辦?
還有一個更大的問題是,這麼做似乎破壞了蘋果對於UINavigationController
的定義,這就使得導航欄在邏輯上成為了單個頁面所獨立持有的個體,在這種情況下倒不如隱藏系統NavigationBar,每個頁面是實現一個自己的導航欄來個更方便維護。
C頁面導航欄跟B頁面一樣?
保持和B頁面一樣,粗略一想,這和“跟A頁面一樣?”方案似乎是差不多的,但實際上這兩種方案有著本質區別。“跟B頁面一樣”換一個更好的說法應該是“跟最近一次使用者對導航欄修改之後的樣式一樣”,也就是說C頁面只需要關注導航欄本身,而不需要關注誰修改了導航欄,這樣一來就滿足來上述的設計思想 “誰用誰修改”。
實現
關於這個庫的實現,筆者在這裡參考了美團點評的這篇文章iOS系統中導航欄的轉場解決方案與最佳實踐。
在轉場的過程中隱藏原有的導航欄並新增假的 NavigationBar,當轉場結束後刪除假的 NavigationBar 並恢復原有的導航欄,這一過程可以通過 Swizzle 的方式完成,而每個 ViewController 只需要關心自身的樣式即可。
DoubleNavigationController核心的解決方案與這篇文章提到的是一樣的,但是在實現方式和細節上可能與文章中提到的並不一樣,另外有一些實現細節在美團點評的這篇文章中並沒有過多地透露。
幾個細節
細節1:DoubleNavigationController中選擇直接NSKeyedArchiver
來複制一個FakeNavigationBar而並沒有自定義UIView。
細節2:有些時候一個頁面的NavigationBar可能會在使用者互動過程中動態變化,因此我們需要記錄每一次使用者對NavigationBar外觀的修改,並在適當的時候對FakeNavigationBar外觀也進行更新。
細節3:由於UIAppearance
的原理是在UIView被新增到檢視樹後才會去改變物件的外觀,因此在使用FakeNavigationBar之前需要再一次和當前的navigationBar進行一次UIAppearance屬性的複製。參考:iOS UIAppearance 探祕 — HyanCat's
例子
clone這個倉庫,進到Example
目錄下執行pod install
來執行一個demo。
用法
通過在ViewController中實現dbn_configNavigationController
這個方法來定製導航欄樣式。
- (void)dbn_configNavigationController:(UINavigationController *)navigationController {
[navigationController setNavigationBarHidden:NO animated:NO];
navigationController.navigationBar.barTintColor = [UIColor whiteColor];
navigationController.navigationBar.tintColor = [UIColor purpleColor];
navigationController.navigationBar.titleTextAttributes = @{NSFontAttributeName: [UIFont systemFontOfSize:20], NSForegroundColorAttributeName: [UIColor redColor]};
}
- (void)dbn_configNavigationItem:(UINavigationItem *)navigationItem {
UIBarButtonItem *btnItem = [[UIBarButtonItem alloc] initWithTitle:@"Next" style:UIBarButtonItemStylePlain target:self action:@selector(eventFromButton:)];
navigationItem.rightBarButtonItem = btnItem;
navigationItem.title = @"Hello";
}
複製程式碼
你還可以使用dbn_performBatchUpdates:
這個方法來隨時更新導航欄樣式。
[self dbn_performBatchUpdates:^(UINavigationController * _Nullable navigationController) {
if (navigationController) {
navigationController.navigationBar.tintColor = [UIColor purpleColor];
}
}];
複製程式碼
專案地址:DoubleNavigationController
其他開源作品
FastKV — iOS的高效能、高實時性key-value持久化元件 github | 掘金
Coolog — 可擴充套件的log框架 github | 掘金