本文是《個人頁的自我修養(yǎng)》系列文章的一篇,全部:
- 導航欄的平滑顯示和隱藏 - 個人頁的自我修養(yǎng)(1) (本篇)
- 多個UITableView共用一個tableHeader的效果實現(xiàn) - 個人頁的自我修養(yǎng)(2)(待補坑)
- 處理Pan手勢和ScrollView滾動沖突 - 個人頁的自我修養(yǎng)(3)(待補坑)
關于“個人頁”
帶有社交屬性的APP一般都會有一個“個人頁”,用以展示某個用戶的個人信息和發(fā)布的內容。以下是幾個例子:

以上頁面的共同特征是:
1、透明的導航欄以更好的展示背景圖
2、可按標簽切換到不同的內容頁(這個特性看需求,不一定有)
3、滾動時會停靠在頁面頂部的SegmentView
3、各個可滾動的內容頁共用一個header
最近剛好寫到Rabo微博客戶端的個人頁的部分,發(fā)現(xiàn)踩到幾個有意思的坑,解決下來決定寫個系列文章把相關解決方法和代碼分享一下。
先看一下要實現(xiàn)的整體效果:

這篇文章先處理導航欄的平滑隱藏和顯示。
導航欄的平滑顯示和隱藏
1、現(xiàn)有解決方案
先看一下手機QQ,是我目前能找到的處理得算比較好的導航欄返回效果。導航欄有跟隨返回手勢透明度漸變的動畫。

但導航欄的返回交互動畫是自定義的,沒有系統(tǒng)自帶的視差效果和毛玻璃效果,而且中斷返回操作的話導航欄會閃一下,影響觀感。

再看一下其他3家的處理方式,他們的處理方法基本一致,都是在進入個人頁時隱藏了系統(tǒng)導航欄,然后添加一個自定義的導航欄,所以過度會比較生硬,與整體的返回效果有斷層。



好,看完以上的例子,輪到我們來實現(xiàn)啦。我們今天的目標是不自定義導航欄,在系統(tǒng)自帶導航欄的基礎上進行非侵入(代碼解耦)的實現(xiàn)。先看效果:

你可以在這里下載本篇文章的代碼:https://github.com/EnderTan/ETNavBarTransparent
2、記錄某個VC的導航欄透明度
對于同一個NavigationController上的ViewController,NavigationBar是全局的,并不能單獨設置某個ViewController的導航欄樣式和屬性。所以我們先給ViewController用擴展添加一個記錄導航欄透明度的屬性:
//ET_NavBarTransparent.swift
extension UIViewController {
fileprivate struct AssociatedKeys {
static var navBarBgAlpha: CGFloat = 1.0
}
var navBarBgAlpha: CGFloat {
get {
let alpha = objc_getAssociatedObject(self, &AssociatedKeys.navBarBgAlpha) as? CGFloat
if alpha == nil {
//默認透明度為1
return 1.0
}else{
return alpha!
}
}
set {
var alpha = newValue
if alpha > 1 {
alpha = 1
}
if alpha < 0 {
alpha = 0
}
objc_setAssociatedObject(self, &AssociatedKeys.navBarBgAlpha, alpha, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
//設置導航欄透明度
navigationController?.setNeedsNavigationBackground(alpha: alpha)
}
}
}
好的,現(xiàn)在可以根據(jù)需要隨時記錄下某個VC的導航欄透明度了,而不會因為push到下個頁面而丟失了這個信息。
3、設置導航欄背景的透明度
要實現(xiàn)上面demo的效果,我們不能修改整個導航欄的透明度,因為導航欄上的NavigationBarItem是需要保留下來的,如果設置整個導航欄的透明度,左右的Item和標題欄都會跟著一起透明了。

然而,系統(tǒng)API并沒有訪問背景View的接口,只好動用下黑魔法了。先看一下導航欄的層級:

首先想到調整第一層_barBackgroundView(_UIBarBackground)的透明度,但試了一下,調整這一層級會丟失毛玻璃效果,效果很突兀:

經(jīng)過測試,調整_backgroundEffectView(-UIVisualEffectView)不會丟失毛玻璃效果:

下面是調整導航欄背景透明度的相關代碼:
//ET_NavBarTransparent.swift
extension UINavigationController {
//Some other code
fileprivate func setNeedsNavigationBackground(alpha:CGFloat) {
let barBackgroundView = navigationBar.value(forKey: "_barBackgroundView") as AnyObject
let backgroundImageView = barBackgroundView.value(forKey: "_backgroundImageView") as? UIImageView
if navigationBar.isTranslucent {
if backgroundImageView != nil && backgroundImageView!.image != nil {
(barBackgroundView as! UIView).alpha = alpha
}else{
if let backgroundEffectView = barBackgroundView.value(forKey: "_backgroundEffectView") as? UIView {
backgroundEffectView.alpha = alpha
}
}
}else{
(barBackgroundView as! UIView).alpha = alpha
}
if let shadowView = barBackgroundView.value(forKey: "_shadowView") as? UIView {
shadowView.alpha = alpha
}
}
}
到這里,我們只要給viewController的擴展屬性navBarBgAlpha賦值,就可以隨意設置導航欄的透明度了。
4、監(jiān)控返回手勢的進度
在手勢返回的交互中,如果前后兩個VC的導航欄透明度不一樣,需要根據(jù)手勢的進度實時調節(jié)透明度。
這里method swizzling一下,用UINavigationController的"_updateInteractiveTransition:"方法監(jiān)控返回交互動畫的進度。
//ET_NavBarTransparent.swift
extension UINavigationController {
//Some other code
open override class func initialize(){
if self == UINavigationController.self {
let originalSelectorArr = ["_updateInteractiveTransition:"]
//method swizzling
for ori in originalSelectorArr {
let originalSelector = NSSelectorFromString(ori)
let swizzledSelector = NSSelectorFromString("et_\(ori)")
let originalMethod = class_getInstanceMethod(self.classForCoder(), originalSelector)
let swizzledMethod = class_getInstanceMethod(self.classForCoder(), swizzledSelector)
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
}
func et__updateInteractiveTransition(_ percentComplete: CGFloat) {
et__updateInteractiveTransition(percentComplete)
let topVC = self.topViewController
if topVC != nil {
//transitionCoordinator帶有兩個VC的轉場上下文
let coor = topVC?.transitionCoordinator
if coor != nil {
//fromVC 的導航欄透明度
let fromAlpha = coor?.viewController(forKey: .from)?.navBarBgAlpha
//toVC 的導航欄透明度
let toAlpha = coor?.viewController(forKey: .to)?.navBarBgAlpha
//計算當前的導航欄透明度
let nowAlpha = fromAlpha! + (toAlpha!-fromAlpha!)*percentComplete
//設置導航欄透明度
self.setNeedsNavigationBackground(alpha: nowAlpha)
}
}
}
}
看一下到這一步的效果:

在手勢交互的過程中,透明度的變化跟預期一樣跟隨手勢變化。但一旦松手,系統(tǒng)會自動完成或取消返回操作,在這一過程中,以上的方法并沒有調用,而導致透明度停留在最后的那個狀態(tài)。
我們需要在UINavigationControllerDelegate中添加邊緣返回手勢松手時的監(jiān)控,還有要處理一下直接點擊返回按鈕和正常Push到新界面時的情況:
//ET_NavBarTransparent.swift
extension UINavigationController:UINavigationControllerDelegate,UINavigationBarDelegate {
public func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
let topVC = navigationController.topViewController
if topVC != nil {
let coor = topVC?.transitionCoordinator
if coor != nil {
//添加對返回交互的監(jiān)控
if #available(iOS 10.0, *) {
coor?.notifyWhenInteractionChanges({ (context) in
self.dealInteractionChanges(context)
})
} else {
coor?.notifyWhenInteractionEnds({ (context) in
self.dealInteractionChanges(context)
})
}
}
}
}
//處理返回手勢中斷對情況
private func dealInteractionChanges(_ context:UIViewControllerTransitionCoordinatorContext) {
if context.isCancelled {
//自動取消了返回手勢
let cancellDuration:TimeInterval = context.transitionDuration * Double( context.percentComplete)
UIView.animate(withDuration: cancellDuration, animations: {
let nowAlpha = (context.viewController(forKey: .from)?.navBarBgAlpha)!
self.setNeedsNavigationBackground(alpha: nowAlpha)
self.navigationBar.tintColor = context.viewController(forKey: .from)?.navBarTintColor
})
}else{
//自動完成了返回手勢
let finishDuration:TimeInterval = context.transitionDuration * Double(1 - context.percentComplete)
UIView.animate(withDuration: finishDuration, animations: {
let nowAlpha = (context.viewController(forKey: .to)?.navBarBgAlpha)!
self.setNeedsNavigationBackground(alpha: nowAlpha)
self.navigationBar.tintColor = context.viewController(forKey: .to)?.navBarTintColor
})
}
}
public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
if viewControllers.count >= (navigationBar.items?.count)! {
//點擊返回按鈕
let popToVC = viewControllers[viewControllers.count-2]
setNeedsNavigationBackground(alpha: (popToVC.navBarBgAlpha))
navigationBar.tintColor = popToVC.navBarTintColor
_ = self.popViewController(animated: true)
}
return true
}
public func navigationBar(_ navigationBar: UINavigationBar, shouldPush item: UINavigationItem) -> Bool {
//push到一個新界面
setNeedsNavigationBackground(alpha: (topViewController?.navBarBgAlpha)!)
navigationBar.tintColor = topViewController?.navBarTintColor
return true
}
}
好的,到這里,對返回和push操作的處理已經(jīng)完成。

5、使用
只需要在隱藏導航欄背景的viewController上把navBarBgAlpha設為0(或其他你需要的值)就可以了:
override func viewDidLoad() {
super.viewDidLoad()
self.navBarBgAlpha = 0
//other code
}
然后在比如tableView滾動到某個位置,需要顯示導航欄時,把navBarBgAlpha設為1(或其他你需要的值)。
6、其他
要達到平滑的轉場效果,還需要對navigationBar的tintColor進行類似的操作,這部分就留給大家自己看一下源碼的相關部分啦。
還有一些細節(jié),比如狀態(tài)欄顏色變化的時機,“preferredStatusBarStyle:”的調用鏈等,也交給大家去發(fā)現(xiàn)和思考了。