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

3D TouchをUINavigationControllerに入れて新しいUXを作ってみた

More than 3 years have passed since last update.

まず始めに

iPhone6s、iPhone6s plusから3D Touchが使えるようになりました。3D Touchを使った表現にはPeek and PopやQuick Actionsなどありますが、それ以外の部分で具体的にどういった部分に導入すれば良いかというものは決まっていない印象があります。いろいろと導入箇所を検討してみたところ、UINavigationControllerの履歴と紐付けたら面白いのではないかと考えました。

そこで、元々リリースしていたSAHistoryNavigationViewControllerというUINavigationControllerの履歴を表示し任意のViewControllerまで戻れるライブラリに、3D Touchを追加してみました。

追加してみた結果のGIFアニメーションが以下になります。
SampleSamples

実装のポイント

以下が実装時のポイントです。

  • UINavigationBarのバックボタンの部分に3D Touchを追加する
  • UINavigationBarのバックボタンのアクションと3D Touchのアクションが衝突しないようにする
  • ViewControllerの遷移と3D Touchの深度を紐付かせるために、カスタムアニメーションを実装しUIPercentDrivenInteractiveTransitionでインタラクティブな動きにする

3D Touchを扱うクラスをつくる

import UIKit.UIGestureRecognizerSubclass
import AudioToolbox.AudioServices

@available(iOS 9, *)
/*
 * UINavigationBarのバックボタンのアクションと3DTouchのアクションが衝突しないようにするために
 * UILongPressGestureRecognizerを継承してminimumPressDurationをしたいからです。
 */
class SAThirdDimensionalTouchRecognizer: UILongPressGestureRecognizer {
    // 3DTouchの深度をパーセンテージにするために使います
    private(set) var percentage: CGFloat = 0
    // 3DTouchのアクションが完了する割合を設定するために使います
    var threshold: CGFloat = 1

    init(target: AnyObject?, action: Selector, threshold: CGFloat) {
        self.threshold = threshold
        super.init(target: target, action: action)
    }

    override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent) {
        super.touchesMoved(touches, withEvent: event)

        guard let touch = touches.first else {
            return
        }
        percentage = max(0, min(1, touch.force / touch.maximumPossibleForce))
        /*
         * minimumPressDurationで設定した秒数後にstateが.Begin -> .Changedになる可能性があるので
         * stateが.Changedだった場合を条件文に含んでいます
         */
        if percentage > threshold && state == .Changed {
            state = .Ended
            // 3DTouchのバイブレーションがなかったので、システムのバイブレーションを使います。
            AudioServicesPlayAlertSound(kSystemSoundID_Vibrate)
        }
    }

    override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent) {
        super.touchesEnded(touches, withEvent: event)
        // touchesMovedでstateを.Endにするので、ここに入った場合は失敗となります
        state = .Failed
    }

    override func reset() {
        super.reset()
        percentage = 0
    }
}

3D Touchを追加する

バックボタンに直接アクションを追加することができないので、まずUINavigationBarに3DTouchが有効になるようにします。バックボタンのアクションと3D Touchが衝突しないようにするために、minimumPressDurationも設定します。

let gestureRecognizer = SAThirdDimensionalTouchRecognizer(target: self, action: "handleThirdDimensionalTouch:", threshold: 0.75)
gestureRecognizer.minimumPressDuration = 0.2
gestureRecognizer.delegate = self
navigationBar.addGestureRecognizer(gestureRecognizer)

UINavigationBarのバックボタンあたりでだけ3D Touchが有効になるようにするために、UIGestureRecognizerDelegateに以下のような実装をします。

extension SAHistoryNavigationViewController: UIGestureRecognizerDelegate {
    public func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool {
        if let _ = visibleViewController?.navigationController?.navigationBar.backItem, view = gestureRecognizer.view as? UINavigationBar {
            var height = 64.0
            if visibleViewController?.navigationController?.navigationBarHidden == true {
                height = 44.0
            }
            let backButtonFrame = CGRect(x: 0.0, y :0.0,  width: 100.0, height: height)
            let touchPoint = gestureRecognizer.locationInView(view)
            if CGRectContainsPoint(backButtonFrame, touchPoint) {
                return true
            }
        }
        return false
    }
}

ViewControllerの遷移と紐付ける

UIViewControllerTransitioningDelegateで任意のUIViewControllerAnimatedTransitioningを実装したクラスのインスタンスを返して、ViewControllerが表示されるときのみインタラクティブな遷移をするように実装します。

extension SAHistoryNavigationViewController : UIViewControllerTransitioningDelegate {
    public func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return SAHistoryViewAnimatedTransitioning(isPresenting: true)
    }

    public func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return SAHistoryViewAnimatedTransitioning(isPresenting: false)
    }

    public func interactionControllerForPresentation(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        // UIPercentDrivenInteractiveTransitionのproperty
        return interactiveTransition
    }
}

3D Touchを検知した際に以下のようにViewControllerを生成して、presentViewControllerした後に、gestureのstateに合わせて処理をしていきます。

@available(iOS 9, *)
func handleThirdDimensionalTouch(gesture: SAThirdDimensionalTouchRecognizer) {
    switch gesture.state {
    case .Began:
        let viewController = ViewController()
        viewController.transitioningDelegate = self
        presentViewController(viewController, animated: true, completion: nil)

    case .Changed:
        // インタラクティブな遷移をさせるために、3D Touchの深度を元にしたパーセンテージを渡しています。
        interactiveTransition?.updateInteractiveTransition(min(gesture.threshold, max(0, gesture.percentage)))

    case .Ended:
        if gesture.percentage >= gesture.threshold {
            interactiveTransition?.finishInteractiveTransition()
        } else {
            interactiveTransition?.cancelInteractiveTransition()
        }

        case .Cancelled, .Failed, .Possible:
            break
        }
    }

最後に

UINavigationControllerのバックボタンに3D Touchを追加してみたところ思っていたよりも不自然さがないので、UINavigationControllerにpushされ続けるとなかなかトップに戻れない問題の新しいユーザー体験の1つになるのではないかと思っております。

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
ユーザーは見つかりませんでした