前言
其實(shí), Apple已經(jīng)提供了navigationController中的控制器都有一個(gè)從屏幕左邊滑動(dòng)pop的手勢(shì), 并且轉(zhuǎn)換控制器之間的各種動(dòng)畫也是已經(jīng)實(shí)現(xiàn)好了, 但是現(xiàn)在很多APP中都有全屏滑動(dòng)返回的功能, 確實(shí)手機(jī)屏幕變大后在一定程度上使用是方便了很多. 暫且不管這種交互設(shè)計(jì)好還是不好, 既然這么多的APP(微博, QQ, 簡(jiǎn)書, 網(wǎng)易新聞...)中都在使用, 肯定在開發(fā)中實(shí)現(xiàn)這個(gè)功能也是必要的了.
最終效果
首先展示一下最終的使用方法, 使用還是比較方便
- 第一種, 使用提供的自定義的navigationController
- 如果在storyboard中使用, 子需要將navigationController設(shè)置為自定義的即可, 默認(rèn)擁有全屏滑動(dòng)返回功能, 如果需要關(guān)閉, 在需要的地方設(shè)置如下即可
// 設(shè)置為true的時(shí)候開啟全屏滑動(dòng)返回功能, 設(shè)置為false, 關(guān)閉
(navigationController as? CustomNavigationController)?.enabledFullScreenPop(isEnabled: false)
- 如果使用代碼初始化, 那么直接使用自定義的navigationController初始化即可
// 同樣的默認(rèn)是開啟全屏滑動(dòng)返回功能的
let navi = CustomNavigationController(rootViewController: rootVc)
//如果需要關(guān)閉或者重新開啟, 在需要的地方使用下面方法
(navigationController as? CustomNavigationController)?.enabledFullScreenPop(isEnabled: false)
- 第二種, 使用提供的navigationController的分類
這種方法, 并沒(méi)有默認(rèn)開啟, 需要我們自己開啟或者關(guān)閉全屏滑動(dòng)返回功能
// 在需要的地方, 獲取到navigationController, 然后使用分類方法開啟(關(guān)閉)全屏返回手勢(shì)即可
navigationController?.zj_enableFullScreenPop(isEnabled: true)
實(shí)現(xiàn)方法: 實(shí)現(xiàn)的方法很多, 比如可以利用系統(tǒng)提供的navigationController的手勢(shì)方法, 利用運(yùn)行時(shí)獲取到這個(gè)手勢(shì)的target和selector, 然后, 我們使用分類或者自定義navigationController在上面添加一個(gè)pan手勢(shì), 將這個(gè)手勢(shì)的target和selector設(shè)置為運(yùn)行時(shí)獲取到系統(tǒng)手勢(shì)的target和selector, 那么, 這個(gè)手勢(shì)就擁有了和系統(tǒng)滑動(dòng)返回相同的效果, 實(shí)現(xiàn)上還是很方便的
但是這里, 我想介紹的是另一種Apple推薦的自定義轉(zhuǎn)場(chǎng)動(dòng)畫的方法,
關(guān)于自定義轉(zhuǎn)場(chǎng)動(dòng)畫的各種知識(shí), 如果你不是很熟悉, 介意大家看看我之前的這篇文章介紹(當(dāng)時(shí)寫就是為了實(shí)現(xiàn)這篇文章鋪墊), 里面介紹了詳細(xì)的自定義教程, 不過(guò)利用是示例了present/dismiss的使用
- 新建一個(gè)ZJNavigationControllerDelegate用于自定義的navigationController的delegate
class ZJNavigationControllerDelegate: NSObject, UINavigationControllerDelegate {
let animator = ZJNavigationControllerAnimator()
let interactive = ZJNavigationControllerInteractiveTransition()
var panGesture: UIPanGestureRecognizer! = nil {
didSet {
interactive.panGesture = panGesture
}
}
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
interactive.navigationController = navigationController
animator.operation = operation
return animator
}
// 這里是手勢(shì)交互動(dòng)畫需要的對(duì)象
func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactive.isInteracting ? interactive : nil
}
// deinit {
// print("\(self.debugDescription) --- 銷毀")
// }
}
- 新建一個(gè)ZJNavigationControllerAnimator繼承自NSObject,并實(shí)現(xiàn)UIViewControllerAnimatedTransitioning協(xié)議, 來(lái)實(shí)現(xiàn)具體的動(dòng)畫
class ZJNavigationControllerAnimator: NSObject, UIViewControllerAnimatedTransitioning {
let duration = 0.35
var operation: UINavigationControllerOperation = .none
func transitionDuration(_ transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(_ transitionContext: UIViewControllerContextTransitioning) {
// fromVc 總是獲取到正在顯示在屏幕上的Controller
let fromVc = transitionContext.viewController(forKey: UITransitionContextFromViewControllerKey)!
// toVc 總是獲取到將要顯示的controller
let toVc = transitionContext.viewController(forKey: UITransitionContextToViewControllerKey)!
let containView = transitionContext.containerView()
let toView: UIView
let fromView: UIView
// Animators should not directly manipulate a view controller's views and should
// use viewForKey: to get views instead.
if transitionContext.responds(to:NSSelectorFromString("viewForKey:")) {
// 通過(guò)這種方法獲取到view不一定是對(duì)應(yīng)controller.view
toView = transitionContext.view(forKey: UITransitionContextToViewKey)!
fromView = transitionContext.view(forKey: UITransitionContextFromViewKey)!
} else {
toView = toVc.view
fromView = fromVc.view
}
// 最終顯示在屏幕上的controller的frame
let visibleFrame = transitionContext.initialFrame(for: fromVc)
// 隱藏在右邊的controller的frame
let rightHiddenFrame = CGRect(origin: CGPoint(x: visibleFrame.width, y: visibleFrame.origin.y) , size: visibleFrame.size)
// 隱藏在左邊的controller的frame
let leftHiddenFrame = CGRect(origin: CGPoint(x: -visibleFrame.width/2, y: visibleFrame.origin.y) , size: visibleFrame.size)
if operation == .push {// push
toView.frame = rightHiddenFrame
fromView.frame = visibleFrame
// 添加toview到最上面(fromView是當(dāng)前顯示在屏幕上的view不用添加)
containView.addSubview(toView)
} else {// pop
fromView.frame = visibleFrame
toView.frame = leftHiddenFrame
// 有時(shí)需要將toView添加到fromView的下面便于執(zhí)行動(dòng)畫
containView.insertSubview(toView, belowSubview: fromView)
}
UIView.animate(withDuration: duration, delay: 0.0, options: [.curveLinear], animations: {
if self.operation == .push {
toView.frame = visibleFrame
fromView.frame = leftHiddenFrame
} else {
fromView.frame = rightHiddenFrame
toView.frame = visibleFrame
}
}) { (_) in
let cancelled = transitionContext.transitionWasCancelled()
if cancelled {
// 如果中途取消了就移除toView(可交互的時(shí)候會(huì)發(fā)生)
toView.removeFromSuperview()
}
// 通知系統(tǒng)動(dòng)畫是否完成或者取消了(必須)
transitionContext.completeTransition(!cancelled)
}
}
// deinit {
// print("\(self.debugDescription) --- 銷毀")
// }
}
- 新建一個(gè)ZJNavigationControllerInteractiveTransition繼承自
UIPercentDrivenInteractiveTransition, 來(lái)處理手勢(shì)的過(guò)程
class ZJNavigationControllerInteractiveTransition: UIPercentDrivenInteractiveTransition {
var panGesture: UIPanGestureRecognizer! = nil {
didSet {
panGesture.addTarget(self, action: #selector(self.handlePan(gesture:)))
}
}
var containerView: UIView!
var navigationController: UINavigationController! = nil {
didSet {
containerView = navigationController.view
containerView.addGestureRecognizer(panGesture)
}
}
var isInteracting = false
override init() {
super.init()
}
func handlePan(gesture: UIPanGestureRecognizer) {
func finishOrCancel() {
let translation = gesture.translation(in: containerView)
let percent = translation.x / containerView.bounds.width
let velocityX = gesture.velocity(in: containerView).x
let isFinished: Bool
// 修改這里可以改變手勢(shì)結(jié)束時(shí)的處理
if velocityX > 100 {
isFinished = true
} else if percent > 0.5 {
isFinished = true
} else {
isFinished = false
}
isFinished ? finish() : cancel()
}
switch gesture.state {
case .began:
isInteracting = true
// pop
if navigationController.viewControllers.count > 0 {
_ = navigationController.popViewController(animated: true)
}
case .changed:
if isInteracting {
let translation = gesture.translation(in: containerView)
var percent = translation.x / containerView.bounds.width
percent = max(percent, 0)
update(percent)
}
case .cancelled:
if isInteracting {
finishOrCancel()
isInteracting = false
}
case .ended:
if isInteracting {
finishOrCancel()
isInteracting = false
}
default:
break
}
}
}
- 最后自定義navigationController
class CustomNavigationController: UINavigationController {
private(set) var panGesture: UIPanGestureRecognizer?
private var customDelegate: CustomNavigationControllerDelegate?
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
enabledFullScreenPop(isEnabled: true)
}
override init(rootViewController: UIViewController) {
super.init(rootViewController: rootViewController)
enabledFullScreenPop(isEnabled: true)
}
init() {
super.init(nibName: nil, bundle: nil)
enabledFullScreenPop(isEnabled: true)
}
// 開啟或者關(guān)閉全屏pop手勢(shì)(默認(rèn)開啟)
func enabledFullScreenPop(isEnabled: Bool) {
if isEnabled {
if customDelegate == nil {
// 創(chuàng)建代理對(duì)象
customDelegate = CustomNavigationControllerDelegate()
// 創(chuàng)建手勢(shì)
panGesture = UIPanGestureRecognizer()
// 傳遞手勢(shì)給代理
customDelegate?.panGesture = panGesture
// 設(shè)置代理為自定義的
delegate = customDelegate
}
} else {
customDelegate = nil
panGesture = nil
delegate = nil
}
}
}
到這里, 實(shí)現(xiàn)的全部過(guò)程就完成了, 如果你對(duì)代碼不是很理解, 建議先去看看自定義轉(zhuǎn)場(chǎng)動(dòng)畫相關(guān)的教程, 或者看看這里.使用效果如圖所示, 當(dāng)然了, 這里并沒(méi)有處理控制器中如果有scrollView的時(shí)候的可能的手勢(shì)沖突, 大家可以自己去嘗試處理一下, 歡迎關(guān)注, 歡迎star.同時(shí)附上Demo地址