iOS Navigation Bar 导航栏折腾记 (Swift&OC)

10,305 阅读5分钟

作为 iOS 开发者,难免要和导航栏打交道,通常呢,像微信这样优秀且友好的应用,全局使用系统导航栏交互效果就非常好了。然而为了更进一步,总是需要更深入地定制化导航栏,包括却不止像(半)透明滑动渐变等交互效果,以及标题颜色偏移还有对应状态栏(StatusBar)的变化。

阅读参考了诸多开发者的经验分享,并发起了一个腾讯投票以了解大多数人是倾向于采用什么样的方式来处理导航栏的问题,于是决定采用系统导航栏+自定义导航栏共用的方式来处理。

发起的iOS导航栏自定义实现方式偏好投票
发起的iOS导航栏自定义实现方式偏好投票

开始折腾之前,先简单说下我对这三种方式的理解。

修改系统导航栏

以添加Catagory(OC)Extension(Swift)重载系统方法等形式,拿到并修改系统导航栏的View,或添加所需要的View来实现自己定制化的需求。

优点:

  1. 实现好后,各控制器定制起来调用方便,往往一两行代码就可以了。
  2. 能够保留侧滑返回的导航栏过渡效果(这个依需求而定,也并完全算优点)

缺点:

  1. 实现方式复杂,涉及系统属性方法的修改,容易遇上各种未知的坑

这种方式可参考这几篇中文分享,写得非常详细:

完全使用自定义导航栏

隐藏系统导航栏,各页面采用自定义导航栏进行需求定制。

优点

  1. 避免系统导航栏存在的各种未知坑
  2. 实现效果可高度自定义,高兴的话可以设计成波浪形,还带动画交互的那种
  3. 一般有些应用采用底部导航栏的设计,基本都是完全使用自定义导航栏实现

缺点

  1. 一般没有系统导航栏的侧滑过渡效果,可参考手淘。(不算完全意义上的缺点)
  2. 依据不同的需求和实现方式,工作量可能较大
  3. 侧滑返回手势、滑动隐藏、触控隐藏等一些系统交互需自行实现
  4. 需要额外处理系统导航栏能够自动处理的in call等系统响应

系统导航栏与自定义导航栏共用

一般来说,一个优秀且友好的应用,多会遵循苹果官方的设计规范,故而绝大多数页面还是能够方便地采用系统导航栏进行处理,此时,部分页面出彩的交互设计,则可以暂时隐藏系统导航栏,采用自定义导航栏进行实现。

优点

  1. 避免修改系统导航栏可能遇到的坑
  2. 仅部分页面针对性采用自定义导航栏,工作量相对可控
  3. 采用系统导航栏的页面之间保留侧滑过渡效果

缺点

  1. 若是需要自定义导航栏的页面较多,工作增量较大
  2. 自定义导航栏页面的侧滑返回等效果需要额外处理

小结

总的来说,三种方式各有优缺,主要还是按照不同的需求采用不同的方案,若是导航栏真的需要水波烂漫的交互效果,侧滑返回的时候还要有个小船划回去,这若非要挑战通过修改系统导航栏的方式实现,费劲踩坑估计在所难免。

开始折腾 - [系统导航栏+自定义导航栏方案]

添加自定义导航栏

fileprivate lazy var customNavigationItem: UINavigationItem = UINavigationItem(title: "Profile")
fileprivate lazy var customNavigationBar: UINavigationBar = {

        let bar = UINavigationBar(frame: CGRect(x: 0, y: 0, width: self.view.bounds.width, height: 64))

        bar.tintColor = UIColor.white
        bar.tintAdjustmentMode = .normal
        bar.alpha = 0
        bar.setItems([self.customNavigationItem], animated: false)

        bar.backgroundColor = UIColor.clear
        bar.barStyle = UIBarStyle.blackTranslucent
        bar.isTranslucent = true
        bar.shadowImage = UIImage()
        bar.setBackgroundImage(UIImage(), for: UIBarMetrics.default)

        let textAttributes = [
            NSForegroundColorAttributeName: UIColor.white,
            NSFontAttributeName: UIFont.systemFont(ofSize: 16)
        ]

        bar.titleTextAttributes = textAttributes

        return bar
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        view.addSubview(customNavigationBar)

        prepareData()

    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        navigationController?.setNavigationBarHidden(true, animated: true)

        // 便于自定义BarButtomItem
        setBackButton()
        customNavigationBar.alpha = 1.0
    }

    func setBackButton() {
        let backBarButtonItem = UIBarButtonItem(image: #imageLiteral(resourceName: "back_white"), style: .plain, target: self, action: #selector(DataProjectDetailViewController.back(_:)))

        self.customNavigationItem.leftBarButtonItem = backBarButtonItem
    }

    @objc fileprivate func back(_ sender: AnyObject) {
        if let presentingViewController = presentingViewController {
            presentingViewController.dismiss(animated: true, completion: nil)
        } else {
            _ = navigationController?.popViewController(animated: true)
        }
    }

若是需要对导航栏进行滑动动画或渐变等处理,则在ScrollView代理方法中对自定义导航栏的属性进行修改。

需要额外强调的是,最好在BaseViewController中对系统导航栏的一些属性做统一初始化处理,以期所有的控制器达到期望的统一效果,以避免自定义页面对系统导航栏的隐藏等修改影响到其它页面的系统导航栏。

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        guard let navigationController = navigationController else {
            return
        }

        // 仅处理导航栏隐藏后重新显示,可在此做更多导航栏的统一效果处理
        if navigationController.isNavigationBarHidden {
            navigationController.setNavigationBarHidden(false, animated: animated)
        }
    }

处理StatusBar状态栏样式

    override var preferredStatusBarStyle : UIStatusBarStyle {
        return UIStatusBarStyle.lightContent
    }

处理边缘侧滑返回

重点!敲黑板、敲黑板了。处理边缘侧滑返回,需要接管实现导航控制器的边缘侧滑返回交互手势代理。好在所有的导航控制器来继承了BaseNavigationController,因而可以在基类进行统一处理。

class BaseNavigationController: UINavigationController {
    override func setNavigationBarHidden(_ hidden: Bool, animated: Bool) {
        super.setNavigationBarHidden(hidden, animated: animated)

        // 接管导航控制器的边缘侧滑返回交互手势代理
        interactivePopGestureRecognizer?.delegate = self
    }
}

extension BaseNavigationController: UIGestureRecognizerDelegate {
    // 让边缘侧滑手势在合适的情况下生效
    func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        if (self.viewControllers.count > 1) {
            return true;
        }
        return false;
    }

    // 允许同时响应多个手势
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true
    }

    // 避免响应边缘侧滑返回手势时,当前控制器中的ScrollView跟着滑动
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return gestureRecognizer.isKind(of: UIScreenEdgePanGestureRecognizer.self)
    }

}

这样就通过自定义添加方式实现了导航栏的定制化,其他页面则继续愉快使用系统导航栏即可。以上就是所有自定义导航栏需要的核心代码了,故没有另外的Demo项目。若是希望继续了解修改系统导航栏的实现方式,可参考文中所提及的几篇分享,强烈推荐。

所以你偏好哪种方式呢?

微信扫一扫,选择属于你的阵营吧!
微信扫一扫,选择属于你的阵营吧!

博客原文链接