Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

はじめに

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

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

本題

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

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

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

以下のような感じです。

SampleSwipeNavigationController.gif

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

AdventCalendar2015/SampleSwipeNavigationController at master · ryokosuge/AdventCalendar2015

ソースコード

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

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)
    }

}
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
    }

}

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

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

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

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

AdventCalendar2015/SampleSwipeNavigationController at master · ryokosuge/AdventCalendar2015

終わりに

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

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

以上です。

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

ryokosuge
金髪iOS エンジニア なんか色々やってます
https://ryokosuge.github.io/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした