38
36

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

iOS Second StageAdvent Calendar 2015

Day 19

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

Last updated at Posted at 2015-12-19

はじめに

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

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

以上です。

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

38
36
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
38
36

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?