分享iOS中实现navigationController全屏手势滑动pop

前言

其实, Apple已经提供了navigationController中的控制器都有一个从屏幕左边滑动pop的手势, 并且转换控制器之间的各种动画也是已经实现好了, 但是现在很多APP中都有全屏滑动返回的功能, 确实手机屏幕变大后在一定程度上使用是方便了很多. 暂且不管这种交互设计好还是不好, 既然这么多的APP(微博, QQ, 简书, 网易新闻...)中都在使用, 肯定在开发中实现这个功能也是必要的了.

最终效果


push.gif

首先展示一下最终的使用方法, 使用还是比较方便

  • 第一种, 使用提供的自定义的navigationController
    • 如果在storyboard中使用, 子需要将navigationController设置为自定义的即可, 默认拥有全屏滑动返回功能, 如果需要关闭, 在需要的地方设置如下即可
// 设置为true的时候开启全屏滑动返回功能, 设置为false, 关闭
        (navigationController as? CustomNavigationController)?.enabledFullScreenPop(isEnabled: false)
storyboard中使用
  • 如果使用代码初始化, 那么直接使用自定义的navigationController初始化即可
        // 同样的默认是开启全屏滑动返回功能的
        let navi = CustomNavigationController(rootViewController: rootVc)
        //如果需要关闭或者重新开启, 在需要的地方使用下面方法
        (navigationController as? CustomNavigationController)?.enabledFullScreenPop(isEnabled: false)
  • 第二种, 使用提供的navigationController的分类
    这种方法, 并没有默认开启, 需要我们自己开启或者关闭全屏滑动返回功能
        // 在需要的地方, 获取到navigationController, 然后使用分类方法开启(关闭)全屏返回手势即可
        navigationController?.zj_enableFullScreenPop(isEnabled: true)

实现方法: 实现的方法很多, 比如可以利用系统提供的navigationController的手势方法, 利用运行时获取到这个手势的target和selector, 然后, 我们使用分类或者自定义navigationController在上面添加一个pan手势, 将这个手势的target和selector设置为运行时获取到系统手势的target和selector, 那么, 这个手势就拥有了和系统滑动返回相同的效果, 实现上还是很方便的
但是这里, 我想介绍的是另一种Apple推荐的自定义转场动画的方法,
关于自定义转场动画的各种知识, 如果你不是很熟悉, 介意大家看看我之前的这篇文章介绍(当时写就是为了实现这篇文章铺垫), 里面介绍了详细的自定义教程, 不过利用是示例了present/dismiss的使用

  1. 新建一个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
    }
    // 这里是手势交互动画需要的对象
    func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        return interactive.isInteracting ? interactive : nil
    }
//    deinit {
//        print("\(self.debugDescription) --- 销毁")
//    }
}
  • 新建一个ZJNavigationControllerAnimator继承自NSObject,并实现UIViewControllerAnimatedTransitioning协议, 来实现具体的动画
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:")) {
            // 通过这种方法获取到view不一定是对应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是当前显示在屏幕上的view不用添加)
            containView.addSubview(toView)
        } else {// pop
            fromView.frame = visibleFrame
            toView.frame = leftHiddenFrame
            // 有时需要将toView添加到fromView的下面便于执行动画
            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(可交互的时候会发生)
                toView.removeFromSuperview()
            }
            // 通知系统动画是否完成或者取消了(必须)
            transitionContext.completeTransition(!cancelled)
        }
    }
//    deinit {
//        print("\(self.debugDescription) --- 销毁")
//    }
}
  • 新建一个ZJNavigationControllerInteractiveTransition继承自
    UIPercentDrivenInteractiveTransition, 来处理手势的过程
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
            
            // 修改这里可以改变手势结束时的处理
            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)
    }
    
    // 开启或者关闭全屏pop手势(默认开启)
    func enabledFullScreenPop(isEnabled: Bool) {
        if isEnabled {
            if customDelegate == nil {
                // 创建代理对象
                customDelegate = CustomNavigationControllerDelegate()
                // 创建手势
                panGesture = UIPanGestureRecognizer()
                // 传递手势给代理
                customDelegate?.panGesture = panGesture
                // 设置代理为自定义的
                delegate = customDelegate
            }
        } else {
            customDelegate = nil
            panGesture = nil
            delegate = nil
        }
    }
}

到这里, 实现的全部过程就完成了, 如果你对代码不是很理解, 建议先去看看自定义转场动画相关的教程, 或者看看这里.使用效果如图所示, 当然了, 这里并没有处理控制器中如果有scrollView的时候的可能的手势冲突, 大家可以自己去尝试处理一下, 欢迎关注, 欢迎star.同时附上Demo地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容