iOS Navigation Bar 導航欄折騰記 (Swift&OC)

Binboy_王興彬發表於2019-03-04

作為 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專案。若是希望繼續瞭解修改系統導航欄的實現方式,可參考文中所提及的幾篇分享,強烈推薦。

所以你偏好哪種方式呢?

微信掃一掃,選擇屬於你的陣營吧!
微信掃一掃,選擇屬於你的陣營吧!

部落格原文連結

相關文章