每个 vc 都有自己的 navigationItem。
// 隐藏 push 的下一个 vc 的 back title。
navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
// 其他赋值没有任何作用
navigationItem.backBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: nil, action: nil)
// 更改当前页面的 title
navigationItem.title = "Hello"
// hidesBackButton 设置为 true 会导致滑动返回失效
navigationItem.hidesBackButton = true
// 默认情况下,设置 leftBarButtonItem 会替换 backBarButtonItem,并且会导致滑动返回消失
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: nil, action: nil)
// 设置 leftItemsSupplementBackButton 为 true 可以规避这种情况
navigationItem.leftItemsSupplementBackButton = true
navigationBar 维护了一个 navigationItem 的栈。
// navigationBar 的 isTranslucent 默认为 true,会有一个毛玻璃透明效果
navigationController?.navigationBar.isTranslucent = false
// 通过 barTintColor 来更改 navigationBar 的颜色
navigationController?.navigationBar.barTintColor = .red
// 通过 tintColor 来更改 navigationItem 上的 barButtomItem 的颜色
navigationController?.navigationBar.tintColor = .red
// 通过 titleTextAttributes 来更改 title 的属性
navigationController?.navigationBar.titleTextAttributes = [NSFontAttributeName: UIFont.systemFont(ofSize: 20)]
// 通过 backIndicatorImage 来统一改变 back button 的图片
navigationController?.navigationBar.backIndicatorImage = UIImage(named: "back")
navigationController.navigationBar.backIndicatorTransitionMaskImage = UIImage(named: "back")
这个需求会经常碰到,但是实现起来却不简单,因为并没有这样的 api 供我们使用,但是能实现的方式也有几种。
第一种,用 leftBarButtonItem 来代替,这种方法的缺点很明显,就是会引起滑动返回失效,并且需要调整 titleEdgeInsets。(用 spacer Item 在 iOS 11 上已经失效了,因为 iOS 11 用了 stackView 来重新实现 items 的排列)
let button = UIButton(frame: CGRect(x: 0, y: 0, width: 100, height: 44))
button.setTitle("back", for: .normal)
button.setTitleColor(.black, for: .normal)
button.contentHorizontalAlignment = .left
button.titleEdgeInsets.left = -8
button.backgroundColor = .red
button.addTarget(self, action: #selector(pop), for: .touchUpInside)
let leftbarbuttonitem = UIBarButtonItem(customView: button)
navigationItem.leftBarButtonItem = leftbarbuttonitem
第二种是遍历子视图来调整位置,这个我没试过,但是这种方法太 tricky,指不定什么时候苹果就换实现了。
最后一种方法是可以让设计给我们切左边留白的图(当然我们也可以自己绘制),这样的话,左边的距离就可以控制了,缺点是没法比预定的位置更小。
extension UINavigationItem {
/// 去掉 backBarButtonItem 的标题
func removeBackTitle() {
backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
}
}
extension UINavigationBar {
/// 设置 backBarButtonItem 的图标并调整位置
///
/// - Parameters:
/// - image: 图标
/// - leftMargin: 左边距边缘的位置
func setBackImage(image: UIImage, leftMargin: CGFloat) {
let realMargin = leftMargin-8
let contextSize = CGSize(width: realMargin+image.size.width, height: image.size.height)
UIGraphicsBeginImageContextWithOptions(contextSize, false, 0)
image.draw(at: CGPoint(x: realMargin, y: 0))
let finalImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
backIndicatorImage = finalImage
backIndicatorTransitionMaskImage = finalImage
}
}
有四种方式:
第一种是在 viewWillAppear 里面调用下面方法,有较好的过渡动画,即 navigationBar 会跟着 VC 移动。注意,如果 navigationBar 从有到无会导致滑动返回失效,从无到有则没有影响。
navigationController?.setNavigationBarHidden(true, animated: true)
第二种方法是设置 isNavigationBarHidden 的值,这种方式不但没有过渡动画,而且会导致滑动返回失效。
navigationController?.isNavigationBarHidden = false
第三种是直接隐藏 navigationBar,没有过渡动画,但不会影响滑动返回。
navigationController?.navigationBar.isHidden = true
第四种是将 navigationBar 变透明,保证 isTranslucent 为 true,然后设置以下代码:
navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
navigationController?.navigationBar.shadowImage = UIImage()
有时候你需要禁用某些页面的滑动返回,最好的做法是这样:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
navigationController?.interactivePopGestureRecognizer.enabled = false
}
override func viewWillDisappear(animated: Bool) {
super.viewDidDisappear(animated)
navigationController?.interactivePopGestureRecognizer.enabled = true
}
前面已经谈到了隐藏 navigationBar 会引起边缘滑动返回失效,另外还有给 leftBarButtonItem 重新赋值也会引起失效。
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: nil, action: nil)
那么如何解决这个问题呢?
navigationController 有个属性叫 interactivePopGestureRecognizer
,就是用来控制滑动返回的,而通过断点、打印和 runtime header,我们知道它的 target 和 delegate 都是 navigationController 的私有属性 __cachedInteractionController
,它的类型是一个私有类 _UINavigationInteractiveTransition
,而且在引起滑动返回失效的 vc 里面查看发现,它的 delegate 和 isEnabled 并没有变化,可以猜想的是在手势的 delegate 方法里做了判断。比如:
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
// 在这里判断手势是否失效
}
所以我们只要在滑动返回失效的 vc 里面将手势的 delegate 置空,然后在其他页面重新赋值为原来的值就行了。
class NavigationViewController: UINavigationController {
// 添加一个属性用来存手势的 delegate
var popDelegate: UIGestureRecognizerDelegate!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
popDelegate = interactivePopGestureRecognizer?.delegate
delegate = self
}
}
extension NavigationViewController: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
// 对于 rootViewController 做特殊处理
guard viewController != navigationController.viewControllers[0] else {
interactivePopGestureRecognizer?.delegate = popDelegate
return
}
if isNavigationBarHidden || viewController.navigationItem.leftBarButtonItem != nil {
interactivePopGestureRecognizer?.delegate = nil
} else {
interactivePopGestureRecognizer?.delegate = popDelegate
}
}
}
这里将它的 delegate 置空可能引起的问题的是,当边缘滑动手势和其他手势冲突的时候,边缘滑动手势也会失效,比如 scrollView 的横向滑动,所以这里还可以为了防止这种情况做处理:
class AvoidGestureScrollView: UIScrollView, UIGestureRecognizerDelegate {
// 防止边缘滑动手势和 scrollView 的滑动手势冲都可以同时被识别
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if otherGestureRecognizer is UIScreenEdgePanGestureRecognizer {
return true
}
return false
}
// 边缘滑动手势优先识别
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if otherGestureRecognizer is UIScreenEdgePanGestureRecognizer {
return true
}
return false
}
}
或者也可以反过来对 UIScreenEdgePanGestureRecognizer 的 delelgate 作处理。
这种方法基本上没有 bug,也没有侵入性。另外还有一个方法是,新建一个手势,通过 KVC 取值,将它的 target 和 action 设为 interactivePopGestureRecognizer
的,这样有一个好处是可以做到全屏返回。这里详细介绍了这种方案。
上面探讨的内容已经给出了一个方案,就是通过在 viewWillAppear 里调用 setNavigationBarHidden 方法来实现有动画的 navigationBar 的有无切换,并且给出了滑动返回失效的解决方案。但是这种方案无法实现 NavigationBar 滑动渐变的需求。
另外一种方案是将 navigationBar 透明后,在需要的界面自己加上假的 navigationBar,这样比较好控制,但不足时需要写一些代码。