[iOS] UIView の高さをドラッグで変え、指を離したときに中途半端な高さだったら特定の高さに吸着させる

こんな感じ

吸着される

方針

  1. ドラッグしたときにUIViewの高さを変える
  2. 指を離したときに上下どちらかに吸着する

ドラッグしたときにUIViewの高さを変える

まず最初に、ドラッグでUIView の高さを変える処理を書きます。

Storyborad に ViewController と対象のUIView を貼り付けます。
以下のように、対象のUIView の制約は下・右・左・高さの4箇所に付与します。

で、次にコードを書いていきます。
対象のUIView と、その高さの制約、2つをIBOutlet でViewController に接続します。

@IBOutlet weak var expandableView: UIView!
@IBOutlet weak var expandableViewConstraintHeight: 

対象のUIView は expandableView というプロパティ名にしてみました。

次に、expandableView でドラッグを検知するために UIGestureRecognizer を付与します。
viewDidLoad などに書きます。

let panGesture = UIPanGestureRecognizer(
    target: self,
    action: #selector(didPan(_:))
)
expandableView.addGestureRecognizer(panGesture) 

ドラッグを検知したときのデリゲートメソッドとして didPan(_:) というメソッドを呼ぶように書きました。
で、そのdidPan(_:) 内の処理は以下のような感じ。

@objc
func didPan(_ recognizer: UIPanGestureRecognizer) {
    let point: CGPoint = recognizer.translation(in: self.view)
    expandableViewConstraintHeight.constant += -(point.y)
         
    recognizer.setTranslation(CGPoint.zero, in: self.view)
} 

これで、UIView をドラッグすると高さが変わるようになりました。

高さに上限・下限を設ける

このままだとUIView の高さが無限に大きくなったりマイナスになったりするので、制約に上限を設けます。
今回は高さの最大は画面いっぱいになるまで、最小は100 としました。

let height: CGFloat = expandableViewConstraintHeight.constant - point.y
expandableViewConstraintHeight.constant = min(max(height, 100.0), 812.0) 

本来ならマジックナンバーではなく計算して数値を求めるべきですが、
今回の本質ではないので簡単のために数値決め打ちです。

次はいよいよ本題、いい感じに吸着させます。

指を離したときに上下どちらかに吸着する

指を離したことの検知

まず指を離したことを検知します。
UIGestureRecognizer.State というEnum でジェスチャーの状態を判定できるので、
switch 文で処理を分岐すればよいです。

@objc
func didPan(_ recognizer: UIPanGestureRecognizer) {
    let point: CGPoint = recognizer.translation(in: self.view)
    let height: CGFloat = expandableViewConstraintHeight.constant - point.y
         
    switch recognizer.state {
        case .changed:
            expandableViewConstraintHeight.constant = min(max(height, 100.0), 812.0)
        case .ended:
            let height: CGFloat = expandableView.frame.size.height > 406.0 ? 812.0 : 100.0
            adjust(height) // アニメーションで吸着させる(後述)
        case .possible, .began, .cancelled, .failed:
            break
        @unknown default:
            break
    }
    
    recognizer.setTranslation(CGPoint.zero, in: self.view)
} 

.changed が指を動かしているときの状態、.end が指を離したことを示します。

指を離したときの上下どちらに吸着するかは、
適当に最大の高さの半分以下なら下、半分以上なら上、としました。

吸着のアニメーション

で、次に吸着のアニメーションです。

func adjust(_ height: CGFloat) {
    expandableViewConstraintHeight.constant = height
    UIView.animate(
        withDuration: 0.5,
        delay: 0.0,
        usingSpringWithDamping: 0.7,
        initialSpringVelocity: 0.0,
        options: [.curveLinear],
        animations: {
            self.view.layoutIfNeeded()
        }
    )
}

animations 内のクロージャですが、self.expandableView.layoutIfNeeded() だとアニメーションが発動しないので要注意です。

で、こんな動きになりました!

吸着される

今回のコードを全て記述すると

こんな感じです!

class SampleViewController: UIViewController {
    @IBOutlet weak var expandableView: UIView!
    @IBOutlet weak var expandableViewConstraintHeight: NSLayoutConstraint!
    override func viewDidLoad() {
        super.viewDidLoad()
         
        let panGesture = UIPanGestureRecognizer(
            target: self,
            action: #selector(didPan(_:))
        )
        expandableView.addGestureRecognizer(panGesture)
    }
     
    @objc
    func didPan(_ recognizer: UIPanGestureRecognizer) {
        let point: CGPoint = recognizer.translation(in: self.view)
        let height: CGFloat = expandableViewConstraintHeight.constant - point.y
         
        switch recognizer.state {
        case .changed:
            expandableViewConstraintHeight.constant = min(max(height, 100.0), 812.0)
        case .ended:
            let height: CGFloat = expandableView.frame.size.height > 406.0 ? 812.0 : 100.0
            adjust(height)
        case .possible, .began, .cancelled, .failed:
            break
        @unknown default:
            break
        }
         
        recognizer.setTranslation(CGPoint.zero, in: self.view)
    }
     
    func adjust(_ height: CGFloat) {
        expandableViewConstraintHeight.constant = height
        UIView.animate(
            withDuration: 0.5,
            delay: 0.0,
            usingSpringWithDamping: 0.7,
            initialSpringVelocity: 0.0,
            options: [.curveLinear],
            animations: {
                self.view.layoutIfNeeded()
            }
        )
    }
} 

最後に

今回は指を離した位置のみで上下どちらに吸着するかを判定していましたが、
UIPanGestureRecognizervelocity メソッドで指の移動速度も取得できるので、
素早くフリックすると上下に吸着する、みたいなこともできます。

以上です。お疲れ様でした!

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中

%d人のブロガーが「いいね」をつけました。