Edited at

【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日目の記事を書き終わりました。

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

以上です。

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