我们在项目中,往往会遇到两个 Navigation Bar 样式不同的问题,如果直接用苹果官方的控件,会出现各种各样的 bug,因为苹果官方的 Navigation Bar 是共用的,所以在两个不同样式的 Navigation Bar 中做转场操作的时候就会出现各种问题。
今天我们就用 Method Swizzling 的方式来修改一下系统的 Navigation Bar 转场时的样式,最终效果如下图所示:
设置导航栏的背景颜色
我们来为 UIViewController
添加一个扩展,用于存储与设置 Navigation Bar
各种属性。首先,我们来为我们的 UIViewController
添加一个 navBarBgColor
的计算属性。 这里运用了 runtime
的关联方法 objc_getAssociatedObject:
和 objc_setAssociatedObject:
来存取所设置的背景颜色。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 extension UIViewController { public var navBarBgColor: UIColor { get { if let color = objc_getAssociatedObject(self , & DefaultValue .navBarBgColor) as? UIColor { return color } return DefaultValue .navBarBgColor } set { objc_setAssociatedObject(self , & DefaultValue .navBarBgColor, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC ) navigationController? .navBarBackgroundColor(newValue) } } }
这时我们就可以存储与设置导航栏的背景色了。
然后我们再为 NavigationController
添加一个扩展,用来调用 Navigationbar
的具体设置方法。
1 2 3 4 5 6 extension UINavigationController { fileprivate func navBarBackgroundColor (_ color : UIColor ) { navigationBar.backgroundColor(color) } }
之后我们在扩展 Navigationbar
,添加具体的设置逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 extension UINavigationBar { fileprivate var backgroundView: UIView ? { get { guard let bgView = objc_getAssociatedObject(self , & DefaultValue .backgroundView) as? UIView else { return nil } return bgView } set { objc_setAssociatedObject(self , & DefaultValue .backgroundView, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC ) } } fileprivate func backgroundColor (_ color : UIColor ) { if backgroundView == nil { setBackgroundImage(UIImage (), for: .default) let height = DeviceInfo .deviceName == .iPhoneX ? 64 : 88 backgroundView = UIView (frame: CGRect (x: 0 , y: 0 , width: Int (bounds.width), height: height)) backgroundView? .autoresizingMask = .flexibleWidth subviews.first? .insertSubview(backgroundView ?? UIView (), at: 0 ) } backgroundView? .backgroundColor = color } }
这里我们是直接插入一个 View
到 NavigationBar
的 _UIBarBackground
中,我们直接在这个 View
上面修改颜色即可。至此,设置 NavigationBar
的背景色告一段落,下来我们来看看转场如何做到颜色的均匀过度。
颜色过度 - runtime
要想做到在右滑进行 pop
操作的时候导航栏颜色均匀的过度需要替换一个系统方法 _updateInteractiveTransition:
,这个方法是用来监听手势的返回进度的,我们可以在这个方法中计算每一个进度时候导航栏颜色的变化。
大家都知道,在 Objective-C
中进行 Method Swizzling
的时候都需要将替换逻辑放在 dispatch_once
中去执行,保证其执行一次。在 Swift 中,现在已经去掉了 dispatch_once
方法,那么我们应该如何做呢?
苹果文档说道,声明为 static let
和 lazy
的变量具有 dispatch_once
的效果。所以我们就可以用更加 Swift 的方式去实现自己的逻辑,不需要去再造一个 dispatch_once
出来。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 private static let swizzle: () = { let needSwizzleSelectorAry = [ NSSelectorFromString ("_updateInteractiveTransition:" ), #selector (popToViewController(_ :animated:)), #selector (popToRootViewController(animated:)) ] let swizzleSelectorAry = [ #selector (em_updateInteractiveTransition(_ :)), #selector (em_popToViewController(_ :animated:)), #selector (em_popToRootViewControllerAnimated(_ :)) ] for sel in needSwizzleSelectorAry { let str = ("em_" + sel.description).replacingOccurrences(of: "__" , with: "_" ) if let originMethod = class_getInstanceMethod(UINavigationController .self , sel), let swizzleMethod = class_getInstanceMethod(UINavigationController .self , Selector (str)) { method_exchangeImplementations(originMethod, swizzleMethod) } } }()
这样就可以实现 dispatch_once
的效果。
接下来又是一个棘手的问题,Swift 中已经没有了 +load
方法怎么办,甚至 Swift 3.1 之后 +initialize
方法都已经不能用了。这可如何是好。经过 Google,发现了一个替代的方案,用 Swift 的协议来进行实现:Handling the Deprecation of initialize()
JORDAN SMITH 想法其实很简单,是通过 runtime
获取到所有类的列表,然后向所有遵循 SelfAware
协议的类发送消息,并且他把这些操作放到了 UIApplication
的 next
属性的调用中,同时发现了 next
属性会在 applicationDidFinishLaunching
之前被调用。
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 private protocol SelfAware : class { static func awake () } private class NothingToSeeHere { static func harmlessFunction () { let typeCount = Int (objc_getClassList(nil , 0 )) let types = UnsafeMutablePointer <AnyClass ?>.allocate(capacity: typeCount) let autoreleasingTypes = AutoreleasingUnsafeMutablePointer <AnyClass >(types) objc_getClassList(autoreleasingTypes, Int32 (typeCount)) for index in 0 ..< typeCount { (types[index] as? SelfAware .Type )? .awake() } types.deallocate() } } extension UIApplication { private static let runOnce: Void = { NothingToSeeHere .harmlessFunction() }() override open var next: UIResponder ? { UIApplication .runOnce return super .next } }
在别人的帮助下,我们也算是优雅的实现了需要的功能,我们将 swizzle
方法放在 awake()
中去执行。
1 2 3 4 5 6 7 extension UINavigationController : SelfAware { static func awake () { guard self !== UINavigationController .self else { return } self .swizzle } }
计算颜色的过度
接下来我们需要一个方法来计算颜色的过度:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 private func averageColor (fromColor : UIColor , toColor : UIColor , percent : CGFloat ) -> UIColor { var fromRed: CGFloat = 0 var fromGreen: CGFloat = 0 var fromBlue: CGFloat = 0 var fromAlpha: CGFloat = 0 fromColor.getRed(& fromRed, green: & fromGreen, blue: & fromBlue, alpha: & fromAlpha) var toRed: CGFloat = 0 var toGreen: CGFloat = 0 var toBlue: CGFloat = 0 var toAlpha: CGFloat = 0 toColor.getRed(& toRed, green: & toGreen, blue: & toBlue, alpha: & toAlpha) let nowRed = fromRed + (toRed - fromRed) * percent let nowGreen = fromGreen + (toGreen - fromGreen) * percent let nowBlue = fromBlue + (toBlue - fromBlue) * percent let nowAlpha = fromAlpha + (toAlpha - fromAlpha) * percent return UIColor (red: nowRed, green: nowGreen, blue: nowBlue, alpha: nowAlpha) }
实现我们自己的 _updateInteractiveTransition:
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @objc func em_updateInteractiveTransition (_ percentComplete : CGFloat ) { guard self .isKind(of: EMNavigationController .self ) else { return } let topVC = self .topViewController if let coor = topVC? .transitionCoordinator, let fromVC = coor.viewController(forKey: .from) as? EMViewController , let toVC = coor.viewController(forKey: .to) as? EMViewController { let fromAlpha = fromVC.navBarBgAlpha let toAlpha = toVC.navBarBgAlpha let nowAlpha = fromAlpha + (toAlpha - fromAlpha) * percentComplete self .navBarBackgroundAlpha(nowAlpha) let fromTintColor = fromVC.navBarTintColor let toTintColor = toVC.navBarTintColor let nowTintColor = averageColor(fromColor: fromTintColor, toColor: toTintColor, percent: percentComplete) self .navBarTintColor(nowTintColor) let fromColor = fromVC.navBarBgColor let toColor = toVC.navBarBgColor let nowColor = averageColor(fromColor: fromColor, toColor: toColor, percent: percentComplete) self .navBarBackgroundColor(nowColor) } em_updateInteractiveTransition(percentComplete) }
至此,我们的过度效果算是搞定一半,还有另外一半就是处理手势取消了。我们需要在 navigationBar:shouldPopItem:
方法中来处理我们的手势取消操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 extension UINavigationController : UINavigationBarDelegate { public func navigationBar (_ navigationBar : UINavigationBar , shouldPop item : UINavigationItem ) -> Bool { if let topVC = topViewController, let coor = topVC.transitionCoordinator, coor.initiallyInteractive { if #available (iOS 10.0 , * ) { coor.notifyWhenInteractionChanges({ (context) in self .dealInteractionChanges(context) }) } else { coor.notifyWhenInteractionEnds({ (context) in self .dealInteractionChanges(context) }) } return true } let itemCount = navigationBar.items? .count ?? 0 let n = viewControllers.count >= itemCount ? 2 : 1 let popToVC = viewControllers[viewControllers.count - n] popToViewController(popToVC, animated: true ) return true } private func dealInteractionChanges (_ context : UIViewControllerTransitionCoordinatorContext ) { let animations: (UITransitionContextViewControllerKey ) -> () = { [weak self ] in if let vc = context.viewController(forKey: $0 ) as? EMViewController { self ? .updateAllStyle(vc) } } if context.isCancelled { let cancelDuration: TimeInterval = context.transitionDuration * Double (context.percentComplete) UIView .animate(withDuration: cancelDuration) { animations(.from) } } else { let finishDuration: TimeInterval = context.transitionDuration * Double (1 - context.percentComplete) UIView .animate(withDuration: finishDuration) { animations(.to) } } } }
现在,我们的导航栏过度效果基本完成,还有一些其他的细节处理,请看我的 GitHub 源码吧。
参考文档
如何优雅地在Swift4中实现Method Swizzling
Handling the Deprecation of initialize()
Swift 3.1 deprecates initialize(). How can I achieve the same thing?
Whither dispatch_once in Swift 3?
导航栏的平滑显示和隐藏 - 个人页的自我修养(1)
超简单!!! iOS设置状态栏、导航栏按钮、标题、颜色、透明度,偏移等