額外知識
在開始寫UINavigationBar
之前,瞭解幾個導航欄中用到的知識,將會更有利於理解。
可單獨使用
首先需要明確UINavigationBar
是可以脫離UINavigationConroller
單獨作為控制元件的。只是UINavigationConroller
建立的 navigationBar 的代理UINavigationBarDelegate
是 navigationConroller 自身。對於程式碼或 IB 直接建立的 navigationBar,代理則需要自己指定。
TintColor
相信大家對tintColor
這個東西肯定不會陌生,這裡就不再累述,只記錄一下本人之前的一個疑惑:UILabel
為什麼不受tintColor
的影響?有位大佬在這裡比較詳細的講解了,我就大概記錄下自己的理解:
Apple
避免在可互動元素上使用邊框和漸變,取而代之使用tintColor
,那麼tintColor
的核心思想就是區分元素是否可以響應觸控。顯而易見的,UILabel
是不可互動元素,即便你設定它的tintColor
也不會被繪製。
VisualEffect
系統有三個關於高斯模糊效果的類,父類:UIVisualEffect
,兩個子類:UIBlurEffect
和UIVibrancyEffect
。
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
後面的檢視新增高斯模糊效果,對於新增到UIVisualEffectView
的contentView
中的檢視則不會產生模糊效果。 -
UIVibrancyEffect
不會給UIVisualEffectView
後面的檢視產生模糊,只會使新增到contentView
中的檢視更加生動。 -
對於
UIBlurEffect
的UIVisualEffectView
,若它的contentView
中又包含了一個UIVibrancyEffect
的UIVisualEffectView
。則顯示效果又有模糊效果,又有生動效果。
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
}()
複製程式碼
文件指出不要給UIVisualEffectView
或者它的父檢視設定小於 1 的alpha
值,否則 effect 可能顯示不正確,或者根本不顯示。但是可以設定contentView
子檢視的alpha
(在已經嘗試過的實際運用中的時候,設定 UIVisualEffectView
的alpha
小於 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
。
- 對於沒有明確設定
isTranslucent
的navigationBar
,如果背景圖alpha < 1
,則isTranslucent = true
。反之為false
。 - 對於明確設定
isTranslucent = true
的,如果背景圖為不透明,則會為背景圖會被新增小於 1 的系統定義的alpha
。 - 對於明確設定
isTranslucent = false
的,如果背景圖alpha < 1
,會根據barStyle
或barTintColor
為該圖片新增一個相應顏色的不透明背景。
背景圖和陰影圖
只有在設定過背景圖片的情況下,陰影圖片才會生效。單獨設定陰影圖片沒有效果。
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 顯示效果不一樣喲):
攔截返回操作
在專案中時常有點選導航欄返回按鈕,彈出確認返回的提示,此時就需要攔截返回事件。
自定義一個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
}
}
複製程式碼