iOS Custom Presentation & Transition (4) 〜Interactive Transition〜
はじめに
いよいよラストになった!!ついつい長引いちゃった。
一応頭の方で説明した方がいいかな。
この記事の内容は以下の記事と繋いている、そして今回はPart4でラストだ。
iOS Custom Presentation & Transition (1) 〜コード一行もない編〜 - your3i’s blog
iOS Custom Presentation & Transition (2) 〜UIPresentationControllerでカスタムモーダルを作る〜 - your3i’s blog
iOS Custom Presentation & Transition (3) 〜Custom Transition〜 - your3i’s blog
前回までは
前回までは、dismissするときのアニメーションをカスタマイズしたところまでだった。そのカスタマイズは今回のinteractive transitionのために必要で、ときにdismiss transitionの見た目は変えてない。
今回の目標は
今回の目標は、dismissするとき、detailsViewController(緑のview)をdrag gestureで閉じれるようにする。
UIPercentDrivenInteractiveTransition
Yeah! これを使って実現する。
UIPercentDrivenInteractiveTransitionプロトコルの役割はtransitionなんパーセント進んだのかの報告やinteractive transitionを完成するかどうかのコントロールだ。
これをadoptするクラスは論理上なんでもよくて、UIViewControllerTransitioningDelegateで返してあげれば問題なし。要するに、DetailsViewController自分でadoptしてもできる。でも今回はinteractorになるCustomDismissInteractorというクラスを作った。
CustomDismissInteractorクラスの作成
とりあえずまずCustomDismissInteractorクラスを作る。
final class CustomDismissInteractor: UIPercentDrivenInteractiveTransition { // まだ空っぽ }
UIViewControllerTransitioningDelegateで返す。DetailsViewControllerの中:
class DetailsViewController: UIViewController, UIViewControllerTransitioningDelegate { private var interactor: CustomDismissInteractor! ... override func viewDidLoad() { super.viewDidLoad() // ここでinteractorを作って保存しておく interactor = CustomDismissInteractor() } ... // MARK: - UIViewControllerTransitioningDelegate func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { return CustomPresentationController(presentedViewController: presented, presenting: presenting) } func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { return CustomDismissAnimator() } func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { // viewDidLoadで作ったinteractorを返す return interactor } }
注意:interactorはなんでviewDidLoadで作らなきゃいけないだろう?interactionControllerForDismissalが呼ばれるたびに作ると、作られたinteractorが誰でもretainしなくて、メソッド完了時点ですぐクリアされる。transitionの進捗も、わかる人いなくなったということだ。だから、detailsViewControllerがinteractorをもつ必要がある。
gesture recognizerをセットアップする前の準備
今回はやることをCustomDismissInteractorに寄せたいので、presentedControllerとどのviewにgestureを追加するのか、をCustomDismissInteractorに渡す。
CustomDismissInteractorに
private var interactiveView: UIView! private var presented: UIViewController! static func instance(_ presented: UIViewController, panOnView view: UIView) -> CustomDismissInteractor { let interactor = CustomDismissInteractor() interactor.interactiveView = view interactor.presented = presented return interactor }
そしたら、DetailsViewControllerでのinteractorのinit方法もこっちに変える:
override func viewDidLoad() { super.viewDidLoad() interactor = CustomDismissInteractor.instance(self, panOnView: view) }
UIPanGestureRecognizerをセットアップ
drag dismissはUIPanGestureRecognizerで実現する。CustomDismissInteractorにgestureを追加するviewを渡したので、その中でgestureのセットアップをやる。
static func instance(_ presented: UIViewController, panOnView view: UIView) -> CustomDismissInteractor { let interactor = CustomDismissInteractor() interactor.interactiveView = view interactor.presented = presented // setupのメソッドを呼び出す interactor.setupPanGesture() return interactor } private func setupPanGesture() { let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:))) panGesture.maximumNumberOfTouches = 1 interactiveView.addGestureRecognizer(panGesture) } @objc func handlePan(_ sender: UIPanGestureRecognizer) { // 次のステップで }
こうして、detailsViewControllerのviewにpan gesture recognizerを追加した。
UIPanGestureRecognizerのハンドリング
@objc func handlePan(_ sender: UIPanGestureRecognizer) { // 0.4パーセント以上完成したらinteractive transition portionを終了 let threshold: CGFloat = 0.4 switch sender.state { case .began: // gestureのstart pointのを設定 sender.setTranslation(.zero, in: interactiveView) // dismiss transitionをスタート(呼ばないとdismiss始まらない) presented.dismiss(animated: true, completion: nil) case .changed: // 進捗pointをとる let translation = sender.translation(in: interactiveView) guard translation.y >= 0 else { // 上にドラッグできなようにしたいから、上にpan gestureが移動したらとにかく.zeroとして認識 sender.setTranslation(.zero, in: interactiveView) return } // パーセンテージを計算 let percentage = abs(translation.y / interactiveView.bounds.height) update(percentage) case .ended: // pan gestureが終了下から、ここはtransitionを完成するかどうかの判断 if percentComplete >= threshold { finish() // 完成するなら、pan gesture recognizerをremove interactiveView.removeGestureRecognizer(sender) } else { cancel() } case .cancelled, .failed: // pan gesture が正しく完了してない場合、transitionをキャンセル cancel() default: break } }
挙動をみる:
drag dismiss できた!!!
でも通常のボタンタップしてdismissするのがこけてる!!
なぜだ!!!!
interactive dismiss と normal dismiss の区別
前のステップで、通常のdismissが動かなくなった原因は:
通常のdismissするときも、animator側(前回の記事を参照)が、これはただのtransitionなのか、それともinteractive transitionなのか、わからないので、UIViewControllerTransitioningDelegateのinteractionControllerForDismissalをとにかく読んでるらしい。今はinteractorを返してるから、interactive transitionだとみられたらしく、アニメーションがはじまらない。
色々書いてて、要するに、ボタンタップからのdismissなのかそれともgesture からはじまったdismiss なのかの判断は、UIViewControllerTransitioningDelegateのinteractionControllerForDismissalの返してる値がnilかどうかによるのだ。
DetailsViewControllerで、interactorがin progessのときだけ、interactorを返すように。
func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { guard interactor.interactionInProgress else { return nil } return interactor }
interactorのinteractionInProgressプロパティがまだないから、追加:
final class CustomDismissInteractor: UIPercentDrivenInteractiveTransition { ... private(set) var interactionInProgress = false ... @objc func handlePan(_ sender: UIPanGestureRecognizer) { let threshold: CGFloat = 0.4 switch sender.state { case .began: // interactive transition portion start interactionInProgress = true sender.setTranslation(.zero, in: interactiveView) presented.dismiss(animated: true, completion: nil) case .changed: let translation = sender.translation(in: interactiveView) guard translation.y >= 0 else { sender.setTranslation(.zero, in: interactiveView) return } let percentage = abs(translation.y / interactiveView.bounds.height) update(percentage) case .ended: if percentComplete >= threshold { finish() interactiveView.removeGestureRecognizer(sender) } else { cancel() } // interactive transition portion end interactionInProgress = false case .cancelled, .failed: cancel() // interactive transition portion end interactionInProgress = false default: // interactive transition portion end interactionInProgress = false } } }
完成!
おわりに
やっと終わった〜
実装も記事を書くのも長かった。
実はまだ細かい改善できるところ書いでないが、まぁいいや、時間があるとき改善編を書こう。
記事のまとめ
iOS Custom Presentation & Transition (1) 〜コード一行もない編〜 - your3i’s blog
iOS Custom Presentation & Transition (2) 〜UIPresentationControllerでカスタムモーダルを作る〜 - your3i’s blog
iOS Custom Presentation & Transition (3) 〜Custom Transition〜 - your3i’s blog
iOS Custom Presentation & Transition (4) 〜Interactive Transition〜 - your3i’s blog