Swift知らないおじさんが突然プロジェクトに放り込まれて七転八倒するストーリー。
やりたいこと
モーダル画面で上がってくるNavigationBarの配下にViewがある。
その子Viewをスワイプして、下方向にのみ追従させる。
指を離した時の移動幅が画面の1/4以上の場合、画面を閉じる。
1/4未満の場合はアニメーションして元の位置に戻す。
方針
Swipe Gestureイベントを使うと、スワイプした時点で画面が閉じてしまう。
子ViewでのPan Gestureイベントを親がDelegateで受け取ればいけるはず
準備
スワイプ中に背後の画面を表示させるため、StoryBoardから、NavigationBarのPresentationを「Over Full Screen」にする。
Delegateを実装してやる。
protocol ChildViewControllerDelegate: class {
//ドラッグイベントを上画面に返す
func viewPan(_ sender: UIPanGestureRecognizer)
}
子ViewにPan Gesture Recognizerイベントを実装し、delegateのviewPanイベントを呼ぶ。
weak var delegate: ChildViewControllerDelegate? = nil
@IBAction func viewPan(_ sender: UIPanGestureRecognizer) {
delegate?.viewPan(sender)
}
NavigationBarControllerの実装(仮)
ChildViewControllerDelegateを継承し、子画面のdelegateにselfを渡しておく(コードは省略)。
元の子画面のY軸位置と、タップしたY軸位置を保持するインスタンス変数を準備してやる。
UIPanGestureRecognizerの各Stateに対して処理を実装する。
private var startY = CGFloat(0)
private var tappedY = CGFloat(0)
func viewPan(_ sender: UIPanGestureRecognizer) {
switch sender.state {
case .began:
// Pan開始時の画面位置と、タップされた位置を保持しておく
self.startY = view.center.y
self.tappedY = sender.location(in: UIScreen.main.focusedView).y
case .changed:
// 保持した値と現在の指の位置を比較し、移動距離を算定する。マイナスの場合はゼロにする
var moveY = sender.location(in: UIScreen.main.focusedView).y - self.tappedY
if moveY < 0 {
moveY = 0
}
self.view.center.y = self.startY + moveY //[A]
case .ended:
// 移動距離が画面サイズの1/4以上の場合、画面を閉じる。
// 未満の場合はアニメーションで元の位置に戻す
let moveY = sender.location(in: UIScreen.main.focusedView).y - self.tappedY
if moveY > CGFloat(view.frame.height / 4) {
dismiss(animated: true, completion: nil)
} else {
UIView.animate(withDuration: 0.2, animations: {
self.view.center.y = self.startY
})
}
default:
break
}
}
問題発生
ところが、上記コードだと、指を動かした瞬間にビューがずれてしまう。
どうやら上部ステータスバー分だけ位置がマイナスされている模様。
そこで、[A]の部分を以下のように変えてみる。
self.view.center.y = self.startY + moveY + UIApplication.shared.statusBarFrame.size.height
指定するy値にステータスバーの高さを加える。うまくいった……と思いきや、指を動かした瞬間・離した瞬間に画面がカクつく。
どうやら、最初のchangedイベントとendedイベントの時のみビューがステータスバーに接続されてしまい、高さがデフォルトで加算されるようだ。
仕方ないので、浮遊状態かどうかをフラグに持たせ、浮遊状態の場合のみステータスバーの高さを加算する。
また、endedイベントでは、浮遊状態フラグが立っている時にステータスバーの高さを減算する。
完成形
private var startY = CGFloat(0)
private var tappedY = CGFloat(0)
private var isDrift = false
func viewPan(_ sender: UIPanGestureRecognizer) {
switch sender.state {
case .began:
// Pan開始時の画面位置と、タップされた位置を保持しておく
self.startY = view.center.y
self.tappedY = sender.location(in: UIScreen.main.focusedView).y
self.isDrift = false
case .changed:
// 保持した値と現在の指の位置を比較し、移動距離を算定する。マイナスの場合はゼロにする
var moveY = sender.location(in: UIScreen.main.focusedView).y - self.tappedY
if moveY < 0 {
//まだ動いていない場合はキャンセルする
if !self.isDrift { return }
moveY = 0
}
//最初の処理ではstatusBarと接続されている
if !self.isDrift {
self.view.center.y = self.startY + moveY
self.isDrift = true
return
}
//次の処理でstatusBarと分離するため、高さを加える
self.view.center.y = self.startY + moveY + UIApplication.shared.statusBarFrame.size.height
case .ended:
// 移動距離が画面サイズの1/4以上の場合、画面を閉じる。
// 未満の場合はアニメーションで元の位置に戻す
let moveY = sender.location(in: UIScreen.main.focusedView).y - self.tappedY
if moveY >= CGFloat(view.frame.height / 4) {
dismiss(animated: true, completion: nil)
} else {
if self.isDrift {
//指を離した時点でstatusBarと繋がるため、高さを引く
self.view.center.y -= UIApplication.shared.statusBarFrame.size.height
}
UIView.animate(withDuration: 0.2, animations: {
self.view.center.y = self.startY
})
}
default:
break
}
}
感想
力技すぎる。絶対もっと楽な方法あるやろ……
諸賢のツッコミお待ちしています。