目次
こんな感じ
方針
- ドラッグしたときにUIViewの高さを変える
- 指を離したときに上下どちらかに吸着する
ドラッグしたときに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()
}
)
}
}
最後に
今回は指を離した位置のみで上下どちらに吸着するかを判定していましたが、UIPanGestureRecognizer
は velocity
メソッドで指の移動速度も取得できるので、
素早くフリックすると上下に吸着する、みたいなこともできます。
以上です。お疲れ様でした!