【iOS】Swipeで簡単に戻れるNavigationControllerを作ってみた


## はじめに

iOS Second Stage Advent Calendar19日目の記事です。

個人的には6記事目になります。

## 本題

iPhoneの画面が大きくなったことでNavigationBarにある戻るボタンが押しづらいという事案をよく聞きます。

確かにホームボタンをダブルタップすれば画面半分を犠牲に届くようになりますが、思いやりを持ちたいところです。

ということで **横にスワイプ** したら簡単に前の画面に戻れるサンプル作りました。

以下のような感じです。

![SampleSwipeNavigationController.gif](https://qiita-image-store.s3.amazonaws.com/0/47302/02d08d1e-9d8d-4c27-824b-b9cdb41f83ad.gif "SampleSwipeNavigationController.gif")


サンプルは以下にあるので良かったら参考にしてください。



[AdventCalendar2015/SampleSwipeNavigationController at master · ryokosuge/AdventCalendar2015](https://github.com/ryokosuge/AdventCalendar2015/tree/master/SampleSwipeNavigationController)

## ソースコード

以下のようなクラスを作って`UINavigationController`を継承したクラスに保持させるだけです。

```swift:NavigationAnimator.swift
import UIKit

protocol NavigationAnimatorDelegate: class {
    
    func popViewController()
    func shouldBeginGesture(gesture: UIGestureRecognizer) -> Bool
    
}

class NavigationAnimator: UIPercentDrivenInteractiveTransition {
    
    private var isPop: Bool = false
    private var percentageDriven: Bool = false
    
    private let Scale: CGFloat = 0.95
    
    weak var delegate: NavigationAnimatorDelegate? = nil
    
    init(view: UIView) {
        super.init()
        setupView(view)
    }
    
}

/// MARK: - UIPanGestureReg
extension NavigationAnimator {
    
    func handlePanGesture(gesture: UIPanGestureRecognizer) {
        guard let view = gesture.view else {
            return
        }
        var percent = gesture.locationInView(view).x / view.bounds.width / 2.0
        percent = percent < 1 ? percent : 0.99
        percentageDriven = true
        switch gesture.state {
        case .Began:
            delegate?.popViewController()
        case .Changed:
            updateInteractiveTransition(percent)
        case .Ended, .Cancelled:
            gesture.velocityInView(view).x < 0 ? cancelInteractiveTransition() : finishInteractiveTransition()
            percentageDriven = false
        default:
            break
        }
    }
    
}

extension NavigationAnimator: UIGestureRecognizerDelegate {
    
    func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool {
        return delegate?.shouldBeginGesture(gestureRecognizer) ?? true
    }
    
}

/// MARK: - UINavigationControllerDelegate
extension NavigationAnimator: UINavigationControllerDelegate {
    
    func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        isPop = operation == UINavigationControllerOperation.Pop
        return self
    }
    
    func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        return percentageDriven ? self : nil
    }
    
}

/// MARK: UIViewControllerAnimatedTransitioning
extension NavigationAnimator: UIViewControllerAnimatedTransitioning {
    
    func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
        return 0.3
    }
    
    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
        guard let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey),
            toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey),
            containerView = transitionContext.containerView() else {
                return
        }
        
        let toView = isPop ? fromViewController.view : toViewController.view
        let fromView = isPop ? toViewController.view : fromViewController.view
        let offset = containerView.frame.width
        
        containerView.insertSubview(toViewController.view, aboveSubview: fromViewController.view)
        if isPop {
            containerView.insertSubview(toViewController.view, belowSubview: fromViewController.view)
        }
        
        toView.frame = containerView.frame
        toView.transform = isPop ? CGAffineTransformIdentity : CGAffineTransformMakeTranslation(offset, 0)
        fromView.frame = containerView.frame
        fromView.transform = isPop ? CGAffineTransformMakeScale(Scale, Scale) : CGAffineTransformIdentity
        
        let aniDuration = transitionDuration(transitionContext)
        UIView.animateWithDuration(aniDuration, animations: {[weak self] () -> Void in
            if let weakSelf = self {
                toView.transform = weakSelf.isPop ? CGAffineTransformMakeTranslation(offset, 0) : CGAffineTransformIdentity
                fromView.transform = weakSelf.isPop ? CGAffineTransformIdentity : CGAffineTransformMakeScale(weakSelf.Scale, weakSelf.Scale)
            }
            }) { (finished) -> Void in
                toView.transform = CGAffineTransformIdentity
                fromView.transform = CGAffineTransformIdentity
                transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
        }
    }
    
}

/// MARK: - private methods.
extension NavigationAnimator {
    
    private func setupView(view: UIView) {
        let gesture = UIPanGestureRecognizer(target: self, action: "handlePanGesture:")
        gesture.delegate = self
        view.addGestureRecognizer(gesture)
    }
    
}
```

```swift:NavigationController.swift

import UIKit

class NavigationViewController: UINavigationController {
    
    var animator: NavigationAnimator!

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
        animator = NavigationAnimator(view: self.view)
        animator.delegate = self
        delegate = animator
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

}

extension NavigationViewController: NavigationAnimatorDelegate {
    
    func popViewController() {
        popViewControllerAnimated(true)
    }
    
    func shouldBeginGesture(gesture: UIGestureRecognizer) -> Bool {
        return viewControllers.count > 1
    }
    
}
```

`UIPercentDrivenInteractiveTransition`と`UIViewControllerAnimatedTransitioning `を継承したクラスを作って、アニメーションの設定をするだけです。

あとはユーザーの指の動きを検知するために`UINavigationController`の`view`に`UIPanGestureRecognizer`を登録して、指のswipeの量で適度に動かしています。

ここら辺は知らなかった時は ~~めんどくさかったですが~~ 難しかったですが、いざ慣れると色々なことに応用できるので、ぜひ触って欲しい機能でもあります。

サンプルは以下にあるので、確認してみてください。

[AdventCalendar2015/SampleSwipeNavigationController at master · ryokosuge/AdventCalendar2015](https://github.com/ryokosuge/AdventCalendar2015/tree/master/SampleSwipeNavigationController)


## 終わりに

これで6日目の記事を書き終わりました。

明日も自分が担当します。

以上です。

ありがとうございました。