iOS - 实现 navigationBar 透明底层 for iOS 11

需求

在某界面里,navigation bar 的主体透明(isTranslucentfalse),但 navigation items 不透明(如 backButtonItem, rightItems)。

在 iOS 11 以前的做法

结合以下代码:

extension UINavigationBar { 
    // Actual type: _UIBarBackground  
    var background: UIView { 
        return subviews[0]
    }
}

直接在 viewWillAppear(_:)viewWillDisappear(_:) 里加动画,改上述 background 属性的 alpha

问题

在 iOS 11 上用这方法无法达到效果,navigation bar 会变回白色或黑色。

失败的解决方案

  • 上述方案
  • 使用 navigationBar.setBackgroundImage(_:forBarMetrics:)navigationBar.shadowImage 的方案

分析

基于上述 extension 的前提下,使用 KVO 找出是什么调用使得 backgroundalpha 改变。

  1. viewDidLoad() 中加入
navigationController?.navigationBar.background.addObserver(self, forKeyPath: "alpha", options: .new, context: nil)
  1. 实现监听方法
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if keyPath == "alpha" {
        // set a breakpoint here
        print("navigationBar.background.alpha = \(navigationController!.navigationBar.background.alpha)")
    }
}
  1. 在上述位置打个断点观察,然后结果是:
    system tracks.png

    结果是系统会在一个私有方法中更新 background 的透明度。

最终解决方案

在系统方法改动 alpha 前,跟我们设置过的 alpha 比较一下,如果我们设过 alpha 为0的,就不许系统设 alpha 为1。这就要我们使用 Method Swizzling 替换 alpha 的 setter 方法了。
需要说明的是,由于动的是系统私有类(_UIBarBackground),下面的这些 extension 都只好拿 UIView 开刀了,然后在必要的地方做一些类型判断。如果你有更好的实现方式,请留言。

extension UIView {
    
    private struct Key {
        static var loyalAlpha = "loyalAlpha"
    }
    
    /// A highly loyal ALPHA who won't be changed by system calls.
    var loyalAlpha: CGFloat? {
        get {
            return objc_getAssociatedObject(self, &Key.loyalAlpha) as? CGFloat
        }
        set {
            objc_setAssociatedObject(self, &Key.loyalAlpha, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
            if let value = newValue {
                alpha = value
            }
        }
    }   
}
extension NSObject {
    /// Exchange two selectors
    ///
    /// - Parameters:
    ///   - originalSelector: The original selector
    ///   - swizzledSelector: A new selector
    class func exchangeImplementations(originalSelector: Selector, swizzledSelector: Selector) {
        guard
            let originalMethod = class_getInstanceMethod(self, originalSelector),
            let swizzledMethod = class_getInstanceMethod(self, swizzledSelector)
            else {
                print("Error: Unable to exchange method implemenation!!")
                return
        }
        method_exchangeImplementations(originalMethod, swizzledMethod)
    }
}

extension UIView {
    class func applySwizzledMethods() {
        exchangeSetAlpha
    }
    
    private static let exchangeSetAlpha: Void = {
        let os = #selector(setter: alpha)
        let ss = #selector(swizzledSetAlpha(_:))
        exchangeImplementations(originalSelector: os, swizzledSelector: ss)
        print("SWIZZLE WARNING: 'exchangeSetAlpha' has been activated!")
    }()
    
    @objc private func swizzledSetAlpha(_ alpha: CGFloat) {
        if type(of: self) == NSClassFromString("_UIBarBackground") {
            if let loyalAlpha = loyalAlpha, loyalAlpha != alpha {
                return
            }
        }
        swizzledSetAlpha(alpha)
    }
}

所以我实现的是一个“非常忠诚的 alpha”,只听我的,不听系统的。但我们要小心使用,不可伤及无辜的 UIView 对象。
用法嘛,还是在 viewWillAppear(_:)viewWillDisappear(_:) 里加动画,但改 background 的是 loyalAlpha

extension UINavigationBar {
    func animateBackground(show: Bool) {
        UIView.animate(withDuration: 0.25) {
            self.background.loyalAlpha = show ? 1 : 0
        }
        if !show {
            self.background.loyalAlpha = nil
        }
    }
}
override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    navigationController?.navigationBar.animateBackground(show: false)
}

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    navigationController?.navigationBar.animateBackground(show: true)
}

最后不要忘记就是,要在 application(_:didFinishLaunchingWithOptions:) 中启动整个 Swizzling 黑魔法。

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.
    UIView.applySwizzledMethods()
    return true
}

最终效果

demo.gif

Give Me a LIKE

https://github.com/Hesse-Huang/LoyalAlphaDemo

你可能感兴趣的:(iOS - 实现 navigationBar 透明底层 for iOS 11)