UIViewのドラッグ (UIGestureRecognizer)


タップイベント検知は、
生のタップイベント情報を操作するゴリゴリの方法もあるが、
基本的なタップイベントに関してはUIGestureRecognizerがテンプレートを用意してくれているので、使えるときはそちらを使いたい。

ドラッグに関しては、UIPanGestureRecognizerと言うテンプレートが用意されている。

実装

UIGestureRecognizerにアクションを設定して、Viewに持たせればおk。

ViewController.swift
import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var draggableView: UIView!

    override func viewDidLoad() {
        super.viewDidLoad()

        let pGR = UIPanGestureRecognizer(target: self, action: #selector(dragging))
        draggableView.addGestureRecognizer(pGR)
    }

    // ドラッグイベント検知で発火するアクション。指をドラッグした分だけviewを移動してみる。
    @objc func dragging(_ p: UIPanGestureRecognizer) {
        guard let v = p.view else {// draggableView
            assert(false)
        }

        switch p.state {// ドラッグのような連続的なタップイベントに対しては、主に開始・途中・終了の状態が取得できるようになっている(他にも取得できる状態はある)。途中だけは一回のタップの中で何度も検知される(当たり前だが)。
        case .began, .changed:
            let diff = p.translation(in: v.superview)// 開始時からの移動差分がUIPanGestureRecognizerのプロパティとして提供されている。
            var c = v.center
            c.x += diff.x
            c.y += diff.y
            v.center = c
            p.setTranslation(.zero, in: v.superview)// 移動差分リセット。今回は参考文献に倣っていちいち差分をとって移動差分をリセットする方法をとってみた。

        default:
            break
        }
    }
}

動作

Videotogif copy.gif

ちなみに下の紫viewは、生のタップイベント情報から実装したドラッグ可能viewなので、興味があれば記事を見てみてください。

原理

ざっくり言うと、
・まずタップイベント情報が該当のUIVIewに送られる。
・UIViewのもつGestureRecognizersプロパティにタップイベント情報が送られる。
・GestureRecognizerがそのタップイベント情報が指定するものかを確認する。今回の場合、タップイベントがドラッグなのかを確認する。
・GestureRecognizerに該当するタップイベントなら、設定されたアクションが発火する。今回ならdragging()が発火する。

おまけ: 元の位置に戻るview

実装

originLocプロパティを追加し、アクションにタップが終了した時の動作を追加した。

ViewController.swift
import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var draggableView: UIView!
    var originLoc: CGPoint!

    override func viewDidLoad() {
        super.viewDidLoad()

        let pGR = UIPanGestureRecognizer(target: self, action: #selector(dragging))
        draggableView.addGestureRecognizer(pGR)
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        originLoc = draggableView.center
    }

    @objc func dragging(_ p: UIPanGestureRecognizer) {
        guard let v = p.view else {// draggableView
            assert(false)
        }

        switch p.state {// ドラッグのような連続的なタップイベントに対しては、主に開始・途中・終了の状態が取得できるようになっている(他にも取得できる状態はある)。途中だけは一回のタップの中で何度も検知される(当たり前だが)。
        case .began, .changed:
            // 前回のイベント検知からの差分分viewを移動させてみる。
            let diff = p.translation(in: v.superview)// 開始時からの移動差分がプロパティとして提供されている。
            var c = v.center
            c.x += diff.x
            c.y += diff.y
            v.center = c
            p.setTranslation(.zero, in: v.superview)// 移動差分リセット。今回は参考文献に倣っていちいち差分をとって移動差分をリセットする方法をとってみた。
        case .ended, .cancelled:// 元の位置に戻してみる。アニメーションを入れるためにゴリゴリになってしまったが、戻すこと自体はaddAnimations{}内の1行で済む。
            let vel = p.velocity(in: v.superview)
            let c = v.center
            let distX = abs(c.x - originLoc.x)
            let distY = abs(c.y - originLoc.y)
            let anim = UIViewPropertyAnimator(
                duration: 0.4,
                timingParameters: UISpringTimingParameters(
                    dampingRatio: 0.6,
                    initialVelocity: CGVector(dx: vel.x/distX, dy: vel.y/distY))
            )
            anim.addAnimations {v.center = self.originLoc}

            anim.startAnimation()
        default:
            break
        }
    }

動作

Videotogif copy 2.gif

参考文献