需求
在某界面里,navigation bar 的主体透明(isTranslucent
为 false
),但 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 找出是什么调用使得 background
的 alpha
改变。
- 在
viewDidLoad()
中加入
navigationController?.navigationBar.background.addObserver(self, forKeyPath: "alpha", options: .new, context: nil)
- 实现监听方法
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)")
}
}
- 在上述位置打个断点观察,然后结果是:
结果是系统会在一个私有方法中更新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
}