分享iOS中實(shí)現(xiàn)navigationController全屏手勢(shì)滑動(dòng)pop

前言

其實(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è)功能也是必要的了.

最終效果


push.gif

首先展示一下最終的使用方法, 使用還是比較方便

  • 第一種, 使用提供的自定義的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)
storyboard中使用
  • 如果使用代碼初始化, 那么直接使用自定義的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的使用

  1. 新建一個(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地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蛔六,一起剝皮案震驚了整個(gè)濱河市腻格,隨后出現(xiàn)的幾起案子呻惕,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,948評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件舌狗,死亡現(xiàn)場(chǎng)離奇詭異础芍,居然都是意外死亡沉填,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門他托,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人知允,你說(shuō)我怎么就攤上這事撤防。” “怎么了咆瘟?”我有些...
    開封第一講書人閱讀 157,490評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵嚼隘,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我袒餐,道長(zhǎng)飞蛹,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,521評(píng)論 1 284
  • 正文 為了忘掉前任灸眼,我火速辦了婚禮卧檐,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘焰宣。我一直安慰自己霉囚,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,627評(píng)論 6 386
  • 文/花漫 我一把揭開白布匕积。 她就那樣靜靜地躺著盈罐,像睡著了一般。 火紅的嫁衣襯著肌膚如雪闪唆。 梳的紋絲不亂的頭發(fā)上盅粪,一...
    開封第一講書人閱讀 49,842評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音苞氮,去河邊找鬼湾揽。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的库物。 我是一名探鬼主播霸旗,決...
    沈念sama閱讀 38,997評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼戚揭!你這毒婦竟也來(lái)了诱告?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,741評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤民晒,失蹤者是張志新(化名)和其女友劉穎精居,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體潜必,經(jīng)...
    沈念sama閱讀 44,203評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡靴姿,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,534評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了磁滚。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片佛吓。...
    茶點(diǎn)故事閱讀 38,673評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖垂攘,靈堂內(nèi)的尸體忽然破棺而出维雇,到底是詐尸還是另有隱情,我是刑警寧澤晒他,帶...
    沈念sama閱讀 34,339評(píng)論 4 330
  • 正文 年R本政府宣布吱型,位于F島的核電站,受9級(jí)特大地震影響陨仅,放射性物質(zhì)發(fā)生泄漏津滞。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,955評(píng)論 3 313
  • 文/蒙蒙 一掂名、第九天 我趴在偏房一處隱蔽的房頂上張望据沈。 院中可真熱鬧,春花似錦饺蔑、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,770評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至隆敢,卻和暖如春发皿,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背拂蝎。 一陣腳步聲響...
    開封第一講書人閱讀 32,000評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工穴墅, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,394評(píng)論 2 360
  • 正文 我出身青樓玄货,卻偏偏與公主長(zhǎng)得像皇钞,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子松捉,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,562評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容