目前,開源社區和業界內已經存在一些 iOS 導航欄轉場的解決方案,但對於歷史包袱沉重的美團 App 而言,這些解決方案並不完美。有的方案不能知足複雜的頁面跳轉場景,有的方案遷移成本較大,爲此咱們提出了一套解決方案並開發了相應的轉場庫,目前該轉場庫已經成爲美團點評多個 App 的基礎組件之一。css
在美團 App 開發的早期,涉及到導航欄樣式改變的需求時,常常會遇到轉場效果不佳或者與預期樣式不符的「小問題」。在業務體量較小的狀況下,爲了知足快速的業務迭代,一般會使用硬編碼的方式來解決這一類「小問題」。但隨着美團 App 業務的高速發展,這種硬編碼的方式遇到了如下的挑戰:ios
從各個角度來看,硬編碼的方式已經不能很好的解決此類問題,美團 App 須要一個更加合理、更加持久、更加簡單易行的解決方案來處理導航欄轉場問題。git
本文將從導航欄的概念入手,經過講解轉場過程當中的狀態管理、轉換時機和樣式變化等內容,引出了在大型應用中導航欄轉場的三種常看法決方案,並對美團點評的解決方案進行剖析。github
在 iOS 系統中, 蘋果公司不只建議開發者遵循 MVC 開發框架,在它們的代碼裏也能夠看到 MVC 的影子,導航欄組件的構成就是一個相似 MVC 的結構,讓咱們先看看下面這張圖:架構
在這張圖裏,咱們能夠將 UINavigationController 看作是 C,UINavigationBar 看作是 V,而 UIViewController 和 UINavigationItem 組成的 Stack 能夠看作是 M。這裏要說明的是,每一個 UIViewController 都有一個屬於本身的 UINavigationItem,也就是說它們是一一對應的。app
UINavigationController 經過驅動 Stack 中的 UIViewController 的變化來實現 View 層級的變化,也就是 UINavigationBar 的改變。而 UINavigationBar 樣式的數據就存儲在 UIViewController 的 UINavigationItem 中。這也就是爲何咱們在代碼裏只要設置 self.navigationItem
的相關屬性就能夠改變 UINavigationBar 的樣式。框架
不少時候,國內的開發者會將 UINavigationBar 和 UINavigationController 混在一塊兒叫導航欄,這樣的作法不只增長了開發者之間的溝通成本,也容易致使誤解。畢竟它們是兩個徹底不同的東西。ide
因此本文爲了更好的闡明問題,會採用英文區分不一樣的概念,當須要描述籠統的導航欄概念時,會使用導航欄組件一詞。oop
經過這一節的回顧,咱們應該明確了 NavigationItem、ViewController、NavigationBar 和 NavigationController 在 MVC 框架下的角色。下面咱們會從新梳理一下導航欄的生命週期和各個相關方法的調用順序。佈局
你們能夠經過下圖得到更爲直觀的感覺,進而瞭解到導航欄組件在 push 過程當中各個方法的調用順序。
值得注意的地方有兩點:
第一個是 UINavigationController 做爲 UINavigationBar 的代理,在沒有特殊需求的狀況下,不該該修改其代理方法,這裏是經過符號斷點獲取它們的調用順序。若是咱們建立了一個自定義的導航欄組件系統,它的調用順序可能會與此不一樣。
第二個是用虛線圈起來的方法,它們也有可能不被調用,這與 ViewController 裏的佈局代碼相關,假設跳轉到新頁面後,新舊頁面中的控件位置會發生變化,或者因爲數據改變驅動了控件之間的約束關係發生變化,這就會帶來新一輪的佈局,進而觸發 viewWillLayoutSubview
和 viewDidLayoutSubview
這兩個方法。固然,具體的調用順序會與業務代碼緊密相關,若是咱們發現順序有所不一樣,也沒必要驚慌。
下面這張圖展現了導航欄在 pop 過程當中各個方法的調用順序:
除了上面說到的兩點,pop 過程當中還須要注意一點,那就是從 B 返回到 A 的過程當中,A 視圖控制器的 viewDidLoad
方法並不會被調用。關於這個問題,只要提醒一下,大多數人都會反應過來是爲何。不過在實際開發過程當中,總會有人忘記這一點。
經過這兩個圖,咱們已經基本瞭解了導航欄組件的生命週期和相關方法的調用順序,這也是後面章節的理論基礎。
導航欄組件在 iOS 11 發佈時,得到了重大更新,這個更新可不是增長了一個大標題樣式(Large Title Display Mode)那麼簡單,須要注意的地方大概有兩點:
導航欄全面支持 Auto Layout 且 NavigationBar 的層級發生了明顯的改變,關於這一點能夠閱讀 UIBarButtonItem 在 iOS 11 上的改變及應對方案 。
因爲引進了 Safe Area 等概念,topLayoutGuide
和 bottomLayoutGuide
等屬性會逐漸廢棄,雖然變化不大,但若是咱們的導航欄在轉場過程當中老是出現視圖上下移動的現象,不妨從這個方面思考一下,若是想深究能夠查看 WWDC 2017 Session 412。
常常有人說 iOS 的原生導航欄組件很差使用,抱怨主要集中在導航欄組件的狀態管理和控件的佈局問題上。
控件的佈局問題隨着 iOS 11 的到來已經變得相對容易處理了很多,但導航欄組件的狀態管理仍然讓開發者頭疼不已。
可能已經有朋友在思考導航欄組件的狀態管理究竟是什麼東西?不要着急,下面的章節就會作相關的介紹。
雖然導航欄組件的 push 和 pop 動畫給人一種每次操做後都會建立一遍導航欄組件的錯覺,但實際上這些 ViewController 都是由一個 NavigationController 所管理,因此你看到的 NavigationBar 是惟一的。
在 NavigationController 的 Stack 存儲結構下,每當 Stack 中的 ViewController 修改了導航欄,勢必會影響其餘 ViewController 展現的效果。
例以下圖所示的場景,若是 NavigationBar 原先的顏色是綠色,但以後進入 Stack 裏的 ViewController 將 NavigationBar 顏色修改成紫色後,在此以後 push 的 ViewController 會從默認的綠色變爲紫色,直到有新的 ViewController 修改導航欄顏色纔會發生變化。
雖然在 push 過程當中,NavigationBar 的變化聽起來合情合理,但若是你在 NavigationBar 爲綠色的 ViewController 裏設置不當的話,那麼當你 pop 回這個 ViewController 時,NavigationBar 可就不必定是綠色了,它還會保持爲紫色的狀態。
經過這個例子,咱們大概會意識到在導航欄裏的 Stack 中,每一個 ViewController 均可以永久的影響導航欄樣式,這種全局性的變化要求咱們在實際開發中必須堅持「誰修改,誰復原」的原則,不然就會形成導航欄狀態的混亂。這不只僅是樣式上的混亂,在一些極端情況下,還有可能會引發 Stack 混亂,進而形成 Crash 的狀況。
咱們剛纔提到了「誰修改,誰復原」的原則,但什麼時候修改,什麼時候復原呢?
對於那些存儲在 Stack 中的 ViewController 而言,它其實就是在不斷的經歷 appear 和 disappear 的過程,結合 ViewController 的生命週期來看,viewWillAppear:
和 viewWillDisappear:
是兩個完美的時間節點,但不少人卻對這兩個方法的調用存在疑惑。
蘋果公司在它的 API 文檔中專門用了一段文字來解答你們的疑惑,這段文字的標題爲《Handling View-Related Notifications》,在這裏咱們直接引用原文:
When the visibility of its views changes, a view controller automatically calls its own methods so that subclasses can respond to the change. Use a method like viewWillAppear: to prepare your views to appear onscreen, and use the viewWillDisappear: to save changes or other state information. Use other methods to make appropriate changes. Figure 1 shows the possible visible states for a view controller’s views and the state transitions that can occur. Not all ‘will’ callback methods are paired with only a ‘did’ callback method. You need to ensure that if you start a process in a ‘will’ callback method, you end the process in both the corresponding ‘did’ and the opposite ‘will’ callback method.
這裏很好的解釋了全部的 will 系列方法和 did 系列方法的對應關係,同時也給咱們吃了一個定心丸,那就是在 appearing 和 disappearing 狀態之間會由 will 系列方法進行銜接,避免了狀態中斷。這對於連續 push 或者連續 pop 的狀況是及其重要的,不然咱們沒法作到 「誰修改,誰復原」的原則。
一般來講,若是隻是一個簡單的導航欄樣式變化,咱們的代碼結構大致會以下所示:
- (void)viewWillAppear:(BOOL)animated{ [super viewWillAppear:animated]; // MARK: change the navigationbar style } - (void)viewWillDisappear:(BOOL)animated{ [super viewWillDisappear:animated]; // MARK: restore the navigationbar style }
如今,咱們明確了修改時機,接下來要明確的就是導航欄的樣式會進行怎樣的變化。
對於不一樣 ViewController 之間的導航欄樣式變化,大多能夠總結爲兩種狀況:
對於顯示與否的問題,能夠在上一節提到的兩個方法裏調用 setNavigationBarHidden:animated:
方法,這裏須要提醒的有兩點:
setNavigationBarHidden:
和 setNavigationBarHidden:animated:
的效果是同樣的,直接使用 setNavigationBarHidden:
會形成導航欄轉場過程當中的閃現、背景錯亂等問題,這一現象在使用手勢驅動轉場的場景中十分常見,因此正確的方式是使用帶有 animated 參數的 API。setNavigationBarHidden:animated:
中的 animated 參數一致。顏色變化的問題就稍微複雜一些,在 iOS 7 後,導航欄增長了 translucent
效果,這使得導航欄背景色的變化出現了兩種狀況:
translucent
屬性值爲 YES 的前提下,更改導航欄的背景色。translucent
屬性值爲 NO 的前提下,更改導航欄的背景色。對於第一種狀況,咱們須要調用 UINavigationBar 的 setBackgroundColor:
方法。
對於第二種狀況咱們須要調用 UINavigationBar 的 setBackgroundImage:forBarMetrics:
方法。
對於第二種狀況,這裏有三點須要提示:
[UIImage new]
建立的對象,無須建立一個顏色爲透明色的圖片。setBackgroundImage:forBarMetrics:
方法的過程當中,若是圖像裏存在 alpha
值小於 1.0 的像素點,則 translucent
的值爲 YES,反之爲 NO。也就是說,若是咱們真的想讓導航欄變成純色且沒有 translucent
效果,請保證全部像素點的 alpha
值等於 1。translucent
屬性設置爲 YES 的話,系統會自動修正這個圖片併爲它添加一個透明度,用於模擬 translucent
效果。translucent
效果爲 NO 的話,那麼系統會在這個帶有透明效果的圖片背後,添加一個不透明的純色圖片用於總體效果的合成。這個純色圖片的顏色取決於 barStyle
屬性,當屬性爲 UIBarStyleBlack
時爲黑色,當屬性爲 UIBarStyleDefault
時爲白色,若是咱們設置了 barTintColor
,則以設置的顏色爲基準。transparent
,translucent
,opaque
,alpha
和 opacity
也挺重要在剛接觸導航欄 API 時,許多人常常會把文檔裏的這些英文詞搞混,也不太明白帶有這些詞的變量爲何有的是布爾型,有的是浮點型,總之一切都讓人很困惑。
在這裏將作了一個總結,這對於理解 Apple 的 API 設計原則十分有幫助。
transparent
, translucent
, opaque
三個詞常常會用在一塊兒,它用於描述物體的透光強度,爲了讓你們更好的理解這三個詞,這裏作了三個比喻:
transparent
是指透明,就比如咱們能夠透過一面乾淨的玻璃清楚的看到外面的風景。translucent
是指半透明,就比如咱們能夠透過一面有點磨砂效果的塑料牆看外面的風景,不能說看不見,但咱們確定看不清。opaque
是指不透明,就比如咱們透過一個堵石牆是看不見任何外面的東西,眼前看到的只有這面牆。這三個詞更多的是用來表述一種狀態,不須要量化,因此這與這三個詞相關的屬性,通常都是 BOOL 類型。
alpha
和 opacity
常常會在一塊兒使用,它要表示的就是透明度,在 Web 端這兩個屬性有着明顯的區別。
在 Web 端裏,opacity
是設定整個元素的透明值,而 alpha
通常是放在顏色設置裏面,因此咱們能夠作到對特定對元素的某個屬性設定 alpha
,好比背景、邊框、文字等。
div { width: 100px; height: 100px; background: rgba(0,0,0,0.5); border: 1px solid #000000; opacity: 0.5; }
這一律念一樣適用於 iOS 裏的概念,好比咱們能夠經過 alpha
通道單獨的去設置 backgroudColor
、borderColor
,它們互不影響,且有着獨立的 alpha
通道,咱們也能夠經過 opacity
統一設置整個 view 的透明度。
但與 Web 端不一致的是,iOS 裏面的 view 不光擁有獨立的 alpha
屬性,同時也是基於 CALayer,因此咱們能夠看到任意 UIView 對象下面都會有一個 layer 的屬性,用於代表 CALayer 對象。view 的 alpha
屬性與 layer 裏面的 opacity
屬性是一個相等的關係,須要注意的是 view 上的 alpha
屬性是 Web 端並不具有的一個能力,因此筆者認爲:在 iOS 中去說 alpha
時,要區分是在說 view 上的屬性,仍是在說顏色通道里的 alpha
。
因爲這兩個詞都是在描述程度,因此咱們看到它們都是 CGFloat 類型:
說完了導航欄的轉場時機和轉場方式,其實大致上你已經能處理好不一樣樣式間的轉換,但還有一些細節須要你去考慮,下面咱們來講說其中須要你關注的兩點。
translucent 會影響導航欄組件裏 ViewController 的 View 佈局,這裏須要你們理清 5 個 API 的使用場景:
edgesForExtendedLayout
extendedLayoutIncluedsOpaqueBars
automaticallyAdjustScrollViewInsets
contentInsetAdjustmentBehavior
additionalSafeAreaInsets
前三個 API 是 iOS 11 以前的 API,它們之間的區別和聯繫在 Stack Overflow 上有一個比較精彩的回答 - Explaining difference between automaticallyAdjustsScrollViewInsets, extendedLayoutIncludesOpaqueBars, edgesForExtendedLayout in iOS7,我在這裏就不作詳細闡述,總結一下它的觀點就是:
若是咱們先定義一個 UINavigationController,它裏面包含了多個 UIViewController,每一個 UIViewController 裏面包含一個 UIView 對象:
edgesForExtendedLayout
是爲了解決 UIViewController 與 UINavigationController 的對齊問題,它會影響 UIViewController 的實際大小,例如 edgesForExtendedLayout
的值爲 UIRectEdgeAll
時,UIViewController 會佔據整個屏幕的大小。automaticallyAdjustsScrollViewInsets
是爲了調整這個 UIScrollView 與 UINavigationController 的對齊問題,這個屬性並不會調整 UIViewController 的大小。edgesForExtendedLayout
來調整 UIViewController 的大小是無效的,這時候你必須使用 extendedLayoutIncludesOpaqueBars
來調整 UIViewController 的大小,能夠認爲 extendedLayoutIncludesOpaqueBars
是基於 automaticallyAdjustsScrollViewInsets
誕生的,這也是爲何常常會看到這兩個 API 會同時使用。這些調整佈局的 API 背後是一套基於 topLayoutGuide
和 bottomLayoutGuide
的計算而已,在 iOS 11 後,Apple 提出了 Safe Area 的概念,將原先分裂開來的 topLayoutGuide
和 bottomLayoutGuide
整合到一個統一的 LayoutGuide 中,也就是所謂的 Safe Area,這個改變看起來彷佛不是很大,但它的出現確實方便了開發者。
若是想對 Safe Area 帶來的改變有更全面的認識,十分推薦閱讀 Rosberry 的工程師 Evgeny Mikhaylov 在 Medium 上的文章 iOS Safe Area,這篇文章基本涵蓋了 iOS 11 中全部與 Safe Area 相關的 API 並給出了真正合理的解釋。
這裏只說一下 contentInsetAdjustmentBehavior
和 additionalSafeAreaInsets
兩個 API。
對於 contentInsetAdjustmentBehavior
屬性而言,它的誕生也意味着 automaticallyAdjustsScrollViewInsets
屬性的失效,因此咱們在那些已經適配了 iOS 11 的工程裏能看到以下相似的代碼:
if (@available(iOS 11.0, *)) { self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; } else { self.automaticallyAdjustsScrollViewInsets = NO; }
此處的代碼片斷只是一個示例,並不適用全部的業務場景,這裏須要着重說明幾個問題:
關於 contentInsetAdjustmentBehavior
中的 UIScrollViewContentInsetAdjustmentAutomatic
的說明一直很「模糊」,經過 Evgeny Mikhaylov 的文章,咱們能夠了解到他在大多數狀況下會與 UIScrollViewContentInsetAdjustmentScrollableAxes
一致,當且僅當知足如下全部條件時纔會與 UIScrollViewContentInsetAdjustmentAlways
類似:
automaticallyAdjustsScrollViewInsets
。iOS 11 後,經過 contentInset
屬性獲取的偏移量與 iOS 10 以前的表現形式並不一致,須要獲取 adjustedContentInset
屬性才能保證與以前的 contentInset
屬性一致,這樣的改變須要咱們在代碼裏對不一樣的版本進行適配。
對於 additionalSafeAreaInsets
而言,若是系統提供的這幾種行爲並不能知足咱們的佈局要求,開發者還能夠考慮使用 additionalSafeAreaInsets
屬性作調整,這樣的設定使得開發者能夠更加靈活,更加自由的調整視圖的佈局。
蘋果提供了許多修改導航欄組件樣式的 API,有關於佈局的,有關於樣式的,也有關於動畫的。backIndicatorImage
和 backIndicatorTransitionMaskImage
就是其中的兩個 API。
backIndicatorImage
和 backIndicatorTransitionMaskImage
操做的是 NavigationBar 裏返回按鈕的圖片,也就是下圖紅色圓圈所標註的區域。
想要成功的自定義返回按鈕的圖標樣式,咱們須要同時設置這兩個 API ,從字面上來看,它們一個是返回圖片自己,另外一個是返回圖片在轉場時用到的 mask 圖片,看起來不怎麼難,咱們寫一段代碼試試效果:
self.navigationController.navigationBar.backIndicatorImage = [UIImage imageNamed:@"backArrow"]; self.navigationController.navigationBar.backIndicatorTransitionMaskImage = [UIImage imageNamed:@"backArrowMask"];
代碼裏的圖片以下所示:
也許大多數人在這裏會都認爲,mask 圖片會遮擋住文字使其在遇到返回按鈕右邊緣的時候就消失。但實際的運行效果是怎麼樣子的呢?咱們來看一下:
在上面的圖片中,咱們能夠看到返回按鈕的文字從返回按鈕的圖片下面穿過而且文字被圖片所遮擋,這種動畫看起來十分奇怪,這是沒法接受的。咱們須要作點修改:
self.navigationController.navigationBar.backIndicatorImage = [UIImage imageNamed:@"backArrow"]; self.navigationController.navigationBar.backIndicatorTransitionMaskImage = [UIImage imageNamed:@"backArrow"];
這一次咱們將 backIndicatorTransitionMaskImage
改成 indicatorImage 所用的圖片。
到這裏,可能大多數人都會好奇,這代碼也能行?讓咱們看下它實際的效果:
在上面的圖中,咱們看到文字在到達圖片的右邊緣時就從下方穿過並被徹底遮蓋住了,這種動畫效果雖然比上面好一些,但仍然有改進的空間,不過這裏咱們先不繼續優化了,咱們先來討論一下它們背後的運做原理。
iOS 系統會將 indicatorImage 中不透明的顏色繪製成返回按鈕的圖標, indicatorTransitionMaskImage 與 indicatorImage 的做用不一樣。indicatorTransitionMaskImage 將自身不透明的區域像 mask 同樣做用在 indicatorImage 上,這樣就保證了返回按鈕中的文字像左移動時,文字只出如今被 mask 的區域,也就是 indicatorTransitionMaskImage 中不透明的區域。
掌握了原理,咱們來解釋下剛纔的兩種現象:
在第一種實現中,咱們提供的 indicatorTransitionMaskImage 覆蓋了整個返回按鈕的圖標,因此咱們在轉場過程當中能夠清晰的看到返回按鈕的文字。
在第二種實現中,咱們使用 indicatorImage 做爲 indicatorTransitionMaskImage,記住文字是隻能出如今 indicatorTransitionMaskImage 裏不透明的區域,因此顯然返回按鈕中的文字會在圖標的最右邊就已經被遮擋住了,由於那片區域是透明的。
那麼前面提到的進一步優化指的是什麼呢?
讓咱們來看一下下面這個示例圖,爲了更好的區分,咱們將 indicatorTransitionMaskImage 用紅色進行標註。黑色仍然是 indicatorImage。
按照剛纔介紹的原理,咱們應該能夠理解,如今文字只會出如今紅色區域,那麼它的實際效果是什麼樣子的呢,咱們能夠看下圖:
如今,一個完美的返回動畫,誕生啦!
此節所用的部分效果圖出自 Ray Wenderlich 的文章 UIAppearance Tutorial: Getting Started
前兩章的鋪墊就是爲了這一章的內容,因此如今讓咱們開始今天的大餐吧。
剛纔咱們說了兩個頁面間 NavigationBar 的樣式變化須要在各自的 viewWillAppear:
和 viewWillDisappear:
中進行設置。那麼問題就來了:這樣的設置會帶來什麼問題呢?
試想一下,當咱們的頁面會跳到不一樣的地方時,咱們是否是要在 viewWillAppear:
和 viewWillDisappear:
方法裏面寫上一堆的判斷呢?若是應用裏還有 router 系統的話,那麼頁面間的跳轉將變得更加不可預知,這時候又該如何在 viewWillAppear:
和 viewWillDisappear:
裏作判斷呢?
如今咱們的問題就來了,如何讓導航欄的轉場更加靈活且相互獨立呢?
常見的解決方案以下所示:
從新實現一個相似 UINavigationController 的容器類視圖管理器,這個容器類視圖管理器作好不一樣 ViewController 間的導航欄樣式轉換工做,而每一個 ViewController 只須要關心自身的樣式便可。
將系統原有導航欄的背景設置爲透明色,同時在每一個 ViewController 上添加一個 View 或者 NavigationBar 來充當咱們實際看到的導航欄,每一個 ViewController 一樣只須要關心自身的樣式便可。
在轉場的過程當中隱藏原有的導航欄並添加假的 NavigationBar,當轉場結束後刪除假的 NavigationBar 並恢復原有的導航欄,這一過程能夠經過 Swizzle 的方式完成,而每一個 ViewController 只須要關心自身的樣式便可。
這三種方案各有優劣,咱們在網上也能夠看到不少關於它們的討論。
例如方案一,雖然看起來工做量大且難度高,可是這個工做一旦完成,咱們就會將處理導航欄轉場的主動權緊緊抓在手裏。但這個方案的一個弊端就是,若是蘋果修改了導航欄的總體風格,就比如 iOS 11 的大標題特效,那麼工做量就來了。
對於方案二而言,雖然看起來簡單易用,但這須要一個良好的繼承關係,若是整個工程裏的繼承關係混亂或者是歷史包袱比較重,後續的維護就像「打補丁」同樣,另外這個方案也須要良好的團隊代碼規範和完善的技術文檔來作輔助。
對於方案三而言,它不須要所謂的繼承關係,使用起來也相對簡單,這對於那些繼承關係和歷史包袱比較重的工程而言,這一個不錯的解決方案,但在解決 Bug 的時候,Swizzle 這種方式無疑會增長解決問題的時間成本和學習成本。
在美團 App 的早期,各個業務方都想充分利用導航欄的能力,但對於導航欄的狀態維護缺少理解與關注,隨着業務方的增長和代碼量的上升,與導航欄相關的問題逐漸暴露出來,此時咱們才意識到這個問題的嚴重性。
大型 App 的導航欄問題就像一個典型的「公地悲劇」問題。在軟件行業,公用代碼的全部權能夠被視做「公地」,由於不注重長期需求而容易遭到消耗。若是開發人員傾向於交付「價值」,而以可維護性和可理解性爲代價,那麼這個問題就特別廣泛了。若是是這種狀況,每次代碼修改將大大減小其整體質量,最終致使軟件的不可維護。
因此解決這個問題的核心在於:明確公用代碼的全部權,並在開發期施加約束。
明確公用代碼的全部權,能夠理解爲將導航欄相關的組件抽離成一個單獨的組件,並交由特定的團隊維護。而在開發期施加約束,則意味着咱們要提供一套完整的解決方案讓各個業務方遵照。
這一節咱們會以美團內部的解決方案爲例,講解如何實現一個流暢的導航欄跳轉過程和相關使用方法。
使用者只用關心當前 ViewController 的 NavigationBar 樣式,而不用在 push 或者 pop 的時候去處理 NavigationBar 樣式。
舉個例子來講,當從 A 頁面 push 到 B 頁面的時候,轉場庫會保存 A 頁面的導航欄樣式,當 pop 回去後就會還原成之前的樣式,所以咱們不用考慮 pop 後導航欄樣式會改變的狀況,同時咱們也沒必要考慮 push 後的狀況,由於這個是頁面 B 自己須要考慮的。
轉場庫的使用十分簡單,咱們不須要 import 任何頭文件,由於它在底層經過 Method Swizzling 進行了處理,只須要在使用的時候遵循下面 4 點便可:
viewDidLoad
或者 viewWillAppear:
方法裏去設置導航欄樣式。setBackgroundImage:forBarMetrics:
方法和 shadowImage
屬性去修改導航欄的背景樣式。viewWillDisappear:
裏添加針對導航欄樣式修改的代碼。隱式修改是指使用
setBackgroundImage:forBarMetrics:
方法時,若是 image 裏的像素點沒有alpha
通道或者alpha
所有等於 1 會使得translucent
變爲 NO 或者 nil。
以上,咱們講完了設計理念和使用方法,那麼咱們來看看美團的轉場庫到底作了什麼?
從大方向上來看,美團使用的是前面所說的第三種方案,不過它也有一些本身獨特的地方,爲了更好的讓你們理解整個過程,咱們設計這樣一個場景,從頁面 A push 到頁面 B,結合以前探討過的方法調用順序,咱們能夠知道幾個核心方法的調用順序大體以下:
pushViewController:animated:
viewDidLoad
or viewWillAppear:
viewWillLayoutSubviews
viewDidAppear:
在 push 過程的開始,轉場庫會在頁面 A 自身的 view 上添加一個與導航欄如出一轍的 NavigationBar 並將真的導航欄隱藏。以後這個假的導航欄會一直存在頁面 A 上,用於保留 A 離開時的導航欄樣式。
等到頁面 B 調用 viewDidLoad
或者 viewWillAppear:
的時候,開發者在這裏自行設置真的導航欄樣式。轉場庫在這裏會對頁面佈局作一些修正和輔助操做,但不會影響導航欄的樣式。
等到頁面 B 調用 viewWillLayoutSubviews
的時候,轉場庫會在頁面 B 自身的 view 上添加一個與真的導航欄如出一轍的 NavigationBar,同時將真的導航欄隱藏。此時不論真的導航欄,仍是假的導航欄都已經與 viewDidLoad
或者 viewWillAppear:
裏設置的同樣的。
固然,這一步也能夠放在
viewWillAppear:
裏並在 dispatch main queue 的下一個 runloop 中處理。
等到頁面 B 調用 viewDidAppear:
的時候,轉場庫會將假的導航欄樣式設置到真的導航欄中,並將假的導航欄從視圖層級中移除,最終將真的導航欄顯示出來。
爲了讓你們更好地理解上面的內容,請參考下圖:
說完了 push 過程,咱們再來講一下從頁面 B pop 回頁面 A 的過程,幾個核心方法的調用順序以下:
popViewControllerAnimated:
viewWillAppear:
viewDidAppear:
在 pop 過程的開始,轉場庫會在頁面 B 自身的 view 上添加一個與導航欄如出一轍的 NavigationBar 並將真的導航欄隱藏,雖然這個假的導航欄會一直存在於頁面 B 上,但它自身會隨着頁面 B 的 dealloc
而消亡。
等到頁面 A 調用 viewWillAppear:
的時候,開發者在這裏自行設置真的導航欄樣式。固然咱們也能夠不設置,由於這時候頁面 A 還持有一個假的導航欄,這裏還保留着咱們以前在 viewDidLoad
裏寫的導航欄樣式。
等到頁面 A 調用 viewDidAppear:
的時候,轉場庫會將假的導航欄樣式設置到真的導航欄中,並將假的導航欄從視圖層級中移除,最終將真的導航欄顯示出來。
一樣,咱們能夠參考下面的圖來理解上面所說的內容:
如今,你們應該對咱們美團的解決方案有了必定的認識,但在實際開發過程當中,還須要考慮一些佈局和適配的問題。
在維護這套轉場方案的時間裏,咱們總結了一些此類方案的最佳實踐。
若是發現導航欄在轉場過程當中出現了樣式錯亂,能夠遵循如下幾點基本原則:
viewDidLoad
和 viewWillAppear:
中,若是在 viewWillDisappear:
等方法裏出現了對導航欄的樣式修改的操做,若是有,請作調整。translucent
屬性,包括顯示修改和隱式修改,若是有,請作調整。永遠記住每一個 ViewController 只用關心本身的樣式,設置的時機點在 viewWillAppear:
或者 viewDidLoad
裏。
若是須要一個透明效果的導航欄,可使用以下代碼實現:
[self.navigationController.navigationBar setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsDefault]; self.navigationController.navigationBar.shadowImage = [UIImage new];
若是須要導航欄實現隨滾動改變總體 alpha
值的效果,能夠經過改變 setBackgroundImage:forBarMetrics:
方法裏 image 的 alpha
值來達到目標,這裏通常是使用監聽 scrollView.contentOffset
的手段來作。請避免直接修改 NavigationBar 的 alpha
值。
還有一點須要注意的是,在頁面轉場的過程當中,也會觸發 contentOffset
的變化,因此請儘可能在 disappear 的時候取消監聽。不然會容易出現導航欄透明度的變化。
請避免背景圖裏的像素點沒有 alpha
通道或者 alpha
所有等於 1,容易觸發 translucent
的隱式改變。
若是咱們須要隱藏導航欄,請保證全部的 ViewController 能堅持以下原則:
viewWillAppear:
中,統一設置導航欄的隱藏狀態。setNavigationBarHidden:animated:
方法,而不是 setNavigationBarHidden:
。若是在轉場的過程當中還會顯示或者隱藏導航欄的話,請保證兩個方法的動畫參數一致。
- (void)viewWillAppear:(BOOL)animated{ [self.navigationController setNavigationBarHidden:YES animated:animated]; }
viewWillAppear:
裏的 animated 參數是受 push 和 pop 方法裏 animated 參數影響。
目前已知的有兩個系統問題以下:
導航欄裏的組件佈局在 iOS 11 後發生了改變,原有的一些解決方案已經失效,這些內容不在本篇文章的討論範圍以內,推薦閱讀UIBarButtonItem 在 iOS 11 上的改變及應對方案,這篇文章詳細的解釋了 iOS 11 裏的變化和可行的應對方案。
本文涉及內容較多,從 iOS 系統下的導航欄概念到大型應用裏的最佳實踐,這裏咱們總結一下整篇文章的核心內容:
特別感謝莫洲騏在此項目裏的貢獻與付出。
思琦,美團點評 iOS 工程師。2016 年加入美團,負責美團平臺的業務開發及 UI 組件的維護工做。
美團平臺誠招 iOS、Android、FE 高級/資深工程師和技術專家,Base 北京、上海、成都,歡迎有興趣的同窗投遞簡歷到zhangsiqi04@meituan.com。