3行で
- インタラクティブに画面を遷移させるには
UIViewControllerTransitioningDelegate
とUIViewControllerAnimatedTransitioning
とUIPercentDrivenInteractiveTransition
の3つが必要です - 今回はドロワーメニューを例にしてインタラクティブな画面遷移のサンプルを作成しました
-
UIViewPropertyAnimator
でさらに効率的に実装できそうだけど今回は挑戦できませんでした
サンプルについて
インタラクティブな画面遷移を試すためにサンプルを作成してGitHubに公開しました。
サンプルは iOS9 まで対応しています。
NavigationDrawerSampler.gif |
---|
サンプルではドロワーメニューを呼び出す画面で NavigationDrawerTransitionCoordinator.swift を初期化してプロパティに保持させています。 NavigationDrawerTransitionCoordinator.swift がドロワーメニューを画面遷移させているクラスです。
import UIKit
import NavigationDrawerTransition
final class ViewController: UIViewController {
private var navigationDrawerTransitionCoordinator: NavigationDrawerTransitionCoordinator?
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "NavigationDrawerSampler"
navigationDrawerTransitionCoordinator = NavigationDrawerTransitionCoordinator(rootViewController: self)
navigationDrawerTransitionCoordinator?.setupDrawerNavigationItemLeftBarButton()
}
}
ドロワーメニューの仕様
ドロワーメニューはiOSのUIというよりは、AndroidのUIです。しかし、Twitterやメルカリなど有名なiOSアプリでも採用されています。
UIKitにはないのでOSSのライブラリを使うか、自分で実装する必要があります。ドロワーメニューのデザインは様々ありますが、おおよそ以下のような仕様になります。
- 画面遷移が途中で停止して全画面の80%の幅でドロワーメニューが表示される
- ドロワーメニューの開閉がドラッグでインタラクティブにできる
- 呼び出し画面(暗い画面)をドラッグまたはタップでドロワーメニューを閉じることができる
- ドロワーメニューが開くのに合わせて呼び出し画面の全体が暗くなり、閉じるのに合わせて元に戻る
実装に必要なプロトコルとクラス
仕様を満たすために画面遷移をカスタマイズして遷移の開始と終了をインタラクティブにさせます。
Appleの公式ドキュメントだと下記のリンクで具体的な実装方法が説明されています。
ドキュメントを要約すると、必要なものは以下の protocol と class です。
- UIViewControllerTransitioningDelegate
- UIViewControllerAnimatedTransitioning
- UIPercentDrivenInteractiveTransition
UIViewControllerTransitioningDelegate
と UIViewControllerAnimatedTransitioning
は遷移をカスタマイズするときに使うプロトコルで、 UIPercentDrivenInteractiveTransition
は遷移をインタラクティブにしてくれるクラスです。
インタラクティブな開閉ジェスチャーはドラッグで実装するので、 UIPercentDrivenInteractiveTransition
は UIPanGestureRecognizer
と合わせて使います。
ドロワーメニューの開閉遷移の処理
ドロワーメニューの開閉遷移の開始、終了、キャンセルで呼ばれるメソッドは、インタラクティブである場合とそうでない場合で少し違います。
詳しくは、サンプルをビルドしてブレークポイントを設定するのが一番わかりやすいので、私が実装中にハマった部分のみ説明します。
ドロワーメニュー表示中に呼び出し元の画面を表示する
/*
* 遷移中のViewに遷移元のViewを貼り付けてスライドしてきた感じを演出
* さらに遷移元のViewにタップとドラッグのジェスチャーを登録したViewを最前面に貼り付けて
* タップとドラッグの両方で画面を閉じることを可能にする
*/
containerView.addSubview(rightView)
self.gestureDarkView?.frame = rightView.frame
containerView.insertSubview(self.gestureDarkView!, aboveSubview: rightView)
AnimationOptionsをcurveLinearにしないと指と動作が合わない
- curveLinearにしないとドラッグしている指を移動させていく距離が伸びるにつれて、想像以上に画面遷移が進んでしまいます。
- イージングに緩急をつけずに等速運動させると回避できそうと思い、curveLinearで対応しました。
画面遷移を途中でキャンセルしたときの処理
- インタラクティブな遷移なので遷移を途中でキャンセルできます。
- キャンセル判定の閾値は、NavigationDrawerInteractiveTransition.swiftの
percentCompleteThreshold
です。 -
finish()
でpercentCompleteThreshold
を引いて、キャンセルしたときに元の画面状態に戻る遷移を自然な状態にします。
<省略>
private let percentCompleteThreshold: CGFloat = 0.2
override func cancel() {
completionSpeed = percentCompleteThreshold
super.cancel()
}
override func finish() {
completionSpeed = 1 - percentCompleteThreshold
super.finish()
}
<省略>
- さらにキャンセルした場合は、遷移を制御している側にもキャンセルしたことを伝える必要があります。
guard !transitionContext.transitionWasCancelled else {
transitionContext.completeTransition(false)
return
}
transitionContext.completeTransition(true)
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
iOS9~11でインタラクティブな画面遷移のとき遷移アニメーションがカクカクする
- iOS9〜11でインタラクティブにドロワーメニューを開閉すると、遷移アニメーションがカクカクする問題が起きました。
- jonkykong/SideMenu でも同じ問題に直面したようで、下記のようなコメントが書かれていました。
- https://github.com/jonkykong/SideMenu/blob/master/Pod/Classes/SideMenuTransition.swift#L510
ViewControllerのライフサイクル
- 遷移をキャンセルして呼び出し画面に戻ると
viewDidAppear:
などのライフサイクルはしっかりと呼ばれます。 - 遷移してることに変わりはないのでライフサイクルには注意してください。
最後に
公式ドキュメントを読めばインタラクティブな画面遷移を実装することはできます。でも、頻繁に見かけるUIで試さないと理解しづらいです。必要なプロトコルやクラスが多く、名前も長いし、処理の順番も把握しにくかったです。画面遷移のカスタマイズは難しい 😕
最近では、ショートカットやマップなどの標準アプリでも画面遷移がとてもカスタマイズされており、画面遷移をカスタマイズする機会はさらに増えそうだなと、この記事を準備していたときは思っていましたが、 UIViewPropertyAnimatorを駆使して作ったもの を読むと標準アプリは画面遷移ではなく、 もしや UIViewPropertyAnimator
で全て実現しているのではとも思い始めています 🤔
画面遷移とアニメーションの奥は深い 🧐