UINavigationBar簡單解析

Natai發表於2019-04-17

額外知識

在開始寫UINavigationBar之前,瞭解幾個導航欄中用到的知識,將會更有利於理解。

可單獨使用

首先需要明確UINavigationBar是可以脫離UINavigationConroller單獨作為控制元件的。只是UINavigationConroller建立的 navigationBar 的代理UINavigationBarDelegate是 navigationConroller 自身。對於程式碼或 IB 直接建立的 navigationBar,代理則需要自己指定。

TintColor

相信大家對tintColor這個東西肯定不會陌生,這裡就不再累述,只記錄一下本人之前的一個疑惑:UILabel為什麼不受tintColor的影響?有位大佬在這裡比較詳細的講解了,我就大概記錄下自己的理解:

Apple避免在可互動元素上使用邊框和漸變,取而代之使用tintColor,那麼tintColor的核心思想就是區分元素是否可以響應觸控。顯而易見的,UILabel是不可互動元素,即便你設定它的tintColor也不會被繪製。

VisualEffect

系統有三個關於高斯模糊效果的類,父類:UIVisualEffect,兩個子類:UIBlurEffectUIVibrancyEffect

UIVisualEffectView就是展示這些效果的檢視,文件裡說:

Depending on the desired effect, the effect may affect content layered behind the view or content added to the visual effect view’s contentView.

對於UIVisualEffectView,根據想要的 effect,

  • UIBlurEffect只是簡單的給UIVisualEffectView後面的檢視新增高斯模糊效果,對於新增到UIVisualEffectViewcontentView中的檢視則不會產生模糊效果。

  • UIVibrancyEffect不會給UIVisualEffectView後面的檢視產生模糊,只會使新增到contentView中的檢視更加生動。

  • 對於UIBlurEffectUIVisualEffectView,若它的contentView中又包含了一個UIVibrancyEffectUIVisualEffectView。則顯示效果又有模糊效果,又有生動效果。

lazy var blurContainVibrancyView: UIVisualEffectView = {
   let vibrancyEffectView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: UIBlurEffect(style: .light)))
   let label = self.creatLabel(withText: "而卒莫消長也")
   label.center = vibrancyEffectView.contentView.center
   vibrancyEffectView.contentView.addSubview(label)

   let blurEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .light))
   blurEffectView.contentView.addSubview(vibrancyEffectView)
   vibrancyEffectView.frame = blurEffectView.frame
   vibrancyEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
   return blurEffectView
}()
複製程式碼

blurContainVibrancyView
blurContainVibrancyView

文件指出不要給UIVisualEffectView或者它的父檢視設定小於 1 的alpha值,否則 effect 可能顯示不正確,或者根本不顯示。但是可以設定contentView子檢視的alpha(在已經嘗試過的實際運用中的時候,設定 UIVisualEffectViewalpha小於 1 時,Xcode 會報警告但是透明和模糊效果都存在。設定它父檢視的alpha小於 1 則沒有警告但是隻有透明效果)。

外觀

導航欄樣式 barStyle

enum UIBarStyle : Int {    
    case`default`
    case black
}
複製程式碼

預設白底黑字,black 樣式為黑底白字。且這兩種樣式都預設半透明(isTranslucent = true)。

barTintColor、tintColor

barTintColor:用來導航欄背景色,不要使用backgroundColor

  • 對於半透明導航欄,設定backgroundColor(藍色),顏色顯示不正確:

半透明設定背景色
半透明設定背景色

  • 對於不透明導航欄,設定backgroundColor,顏色完全不顯示:

不透明設定背景色
不透明設定背景色

tintColor:影響 bar 的子檢視顏色。

標題文字樣式

  • titleTextAttributes:常見的NSAttributedString設定。
  • setTitleVerticalPositionAdjustment(CGFloat, for: UIBarMetrics):標題豎直方向偏移量。

isTranslucent

影響navigationBar的半透明效果,預設為true

  • 對於沒有明確設定isTranslucentnavigationBar,如果背景圖alpha < 1,則isTranslucent = true。反之為false
  • 對於明確設定isTranslucent = true的,如果背景圖為不透明,則會為背景圖會被新增小於 1 的系統定義的alpha
  • 對於明確設定isTranslucent = false的,如果背景圖alpha < 1,會根據barStylebarTintColor為該圖片新增一個相應顏色的不透明背景。

背景圖和陰影圖

只有在設定過背景圖片的情況下,陰影圖片才會生效。單獨設定陰影圖片沒有效果。

shadowImage的位置實際上是超出了它的父檢視的,設定navigationBar.clipsToBounds = true也可以隱藏。

假設isTranslucent = true

  • 如果沒有背景圖片,navigationBar的子檢視中將會包含一個visualEffectView用來產生模糊效果。

預設檢視層級
預設檢視層級

  • 如果設定了背景圖片,navigationBar的子檢視中將不會包含visualEffectView,而是直接生成一個半透明的背景圖。

設定背景圖後的檢視層級
設定背景圖後的檢視層級

代理

導航欄位置 barPosition

public enum UIBarPosition : Int {
    case any // 未指明的
    case bottom // 指定 bar 在父檢視的底部,各種陰影都會被繪製在 bar 頂部
    case top // 指定 bar 在父檢視的頂部,各種陰影都會被繪製在 bar 底部
    case topAttached // 指定 bar 和父檢視都在螢幕的頂部,並且 bar 的背景會穿透狀態列
}
複製程式碼

barPosition其實是協議UIBarPositioning中定義的屬性,UINavigationBar預設遵守了該協議,值為.top

開篇就說到,UINavigationConroller建立的 navigationBar,代理為 navigationConroller 自身。其預設實現為.topAttached

如果自己建立一個 navigationBar 並將其新增到當前控制器檢視中,指定代理為當前控制器。並實

UINavigationBarDelegate

func position(for bar: UIBarPositioning) -> UIBarPosition {
    return .topAttached
}
複製程式碼

可以得到和原生同樣的效果(圖中系統 iOS 10,高度為自定義,iOS 11 顯示效果不一樣喲):

自己建立UINavigationBar
自己建立UINavigationBar

攔截返回操作

在專案中時常有點選導航欄返回按鈕,彈出確認返回的提示,此時就需要攔截返回事件。

自定義一個NavigationBarShouldPopProtocol將是否可以 pop 的控制許可權交給當前控制器,再修改UINavigationController的預設實現,每次都詢問topViewController是否可以 pop。且我們可以在shouldPopWhenClickBackButton方法中做一些額外操作(比如返回false,彈出提示框)。

protocol NavigationBarShouldPopProtocol {
    func shouldPopWhenClickBackButton() -> Bool
}

// 點選 navigationBar 的 backButton 是否 pop,預設為 true
extension UIViewController: NavigationBarShouldPopProtocol {
    @objc func shouldPopWhenClickBackButton() -> Bool {
        return true
    }
}
複製程式碼
extension UINavigationController: UINavigationBarDelegate {
    public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
        guard let items = navigationBar.items else {
            return false
        }
        
        if viewControllers.count < items.count {
            return true
        }
        
        var shouldPop = true
        if let controller = topViewController, controller.responds(to: #selector(UIViewController.shouldPopWhenClickBackButton)) {
			// 詢問是否可以 pop
            shouldPop = controller.shouldPopWhenClickBackButton()
        }
        
        if shouldPop {
            DispatchQueue.main.async {
                self.popViewController(animated: true)
            }
        } else {
            for view in navigationBar.subviews {
                if view.alpha > 0 && view.alpha < 1 {
                    view.alpha = 1
                }
            }
        }
        
        return false
    }
}
複製程式碼

相關文章