讓UINavigationController更好用

RyanLeeLY發表於2019-04-12

去年看到過美團點評技術團隊的一篇文章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

也就是說UINavigationController是作為UIViewController的管理者,因此它的NavigationBar不應該從屬於任何一個ViewController。但是大部分UI設計者都沒有明白蘋果設計的用意,因此在業務中經常出現一個場景:在邏輯上應該從屬於同一個NavigationController多個ViewController卻擁有不同的NavigationBar樣式

不同頁面的開發者只能看到對自己開發的便利性,對UINavigationController的理解不到位,到處修改NavigationBar樣式,在頁面轉場過程中NavigationBar出現了各種不可控的問題。這個問題在App支援路由後會變得更為突出,原因是各個頁面的跳轉關係將會非常複雜且不可預知。

DoubleNavigationController解決的便是這個問題,讓開發者自由地修改NavigationBar樣式,並且不用擔心在退回到棧下ViewController後NavigationBar的樣式也被修改。簡單來說就是,我們修改NavigationBar不再會影響棧內現有的頁面樣式,而隻影響之後Push的新頁面。

Example

  • 為什麼不設計成既不影響棧內現有的頁面樣式,也不影響新頁面?

在這裡先講一下DoubleNavigationController的兩個設計思想 “先到先得”“誰用誰修改”

“先到先得”

先出現的頁面樣式不應該受到後出現的頁面影響。使用者在使用過程中先看到了頁面A的樣式,接著從A頁面跳轉到B頁面,B頁面的導航欄樣式與A頁面不同,這時使用者再返回A頁面,從正常邏輯上來說,使用者希望看到的A頁面導航欄應該還是之前見過的樣式,不應該受到B的影響而改變。

Example

“誰用誰修改”

繼續上面一個場景,使用者從頁面A到頁面B,再從頁面B跳轉到頁面C,在上一個場景下我們知道,頁面B修改了導航欄樣式,使其與頁面A不同,當我們跳轉到頁面C時,此時存在如下兩種可能:

  1. 頁面C也對剛才B頁面修改過的導航欄樣式屬性進行了修改。

  2. 頁面C沒有對剛才B頁面修改過的導航欄樣式屬性進行修改。

在第1種情況下我們很容易確定,C頁面的導航欄樣式就應該是C頁面自己修改的樣式。那麼在第2種情況下,C頁面導航欄應該長什麼樣?

Example

再考慮以下3種方案:跟A頁面一樣?跟UIAppearance配置一樣?跟B頁面一樣?

C頁面導航欄跟A頁面一樣?

這個方案在邏輯上就是錯誤的,因為C頁面根本不應該關心它的上上一個頁面樣式。如下圖,假設B頁面有兩條跳轉路徑A1和A2,此時C頁面的樣式就有2種可能,相信絕大多數App的設計都不會出現 “1個頁面,2種UI” 的情況吧。

Example

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

Example

例子

clone這個倉庫,進到Example目錄下執行pod install來執行一個demo。

Example

用法

通過在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

其他開源作品

TinyPart — 模組化框架 github | 掘金

FastKV — iOS的高效能、高實時性key-value持久化元件 github | 掘金

Coolog — 可擴充套件的log框架 github | 掘金

參考

iOS系統中導航欄的轉場解決方案與最佳實踐

UIKit UIAppearance - APPLE

iOS UIAppearance 探祕 — HyanCat's

相關文章