额外知识
在开始写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,默认为 trueextension 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 }}复制代码