前説
今回は UIViewControllerTransitioningDelegate
や UIViewControllerAnimatedTransitioning
の
使い方についてまとめていきます。
前回の記事 でいい感じに動画コンテンツみたくインライン表示、フルスクリーン表示を切り替えることができました。
しかし、フルスクリーン用VCが遷移周りの処理も請け負ってしまい 神クラスっぽくなってしまいました。
やばそうな雰囲気を感じるので、遷移に関する責務を分離します!
前提条件
Xcode: 12.1
シミュレータ: iOS14系
というわけで早速作っていきます。
主な登場クラス
今回以下の4つになります。
- インライン用VC: 今回遷移前のVC
- フルスクリーン用VC: 今回遷移後のVC
- UIViewControllerTransitioningDelegate: 遷移時にどの UIViewControllerAnimatedTransitioning (↓これ) を利用するかを定義する
- UIViewControllerAnimatedTransitioning: 遷移時の具体的なアニメーションを定義する
Transitioning ほげほげクラスは何かややこしくて最初めっちゃ混乱しがちですが、実装が必要なクラスはUIViewControllerTransitioningDelegate
と UIViewControllerAnimatedTransitioning
の2種類のみです🙌
画面構成

流れをシーケンス図で表示するとこうなる
インライン -> フルスクリーン

フルスクリーン -> インライン

上図の絵の列の「コンテナビュー」は、遷移時にiOSが自動で生成するUIView です(後述)
コードの概要
まず遷移前後における、各クラスの役割と内容をざっくり書きます。
役割がざっくり分かれば Transitioning 系の使い方をイメージできると思います!
1.インライン用VC
// フルスクリーン用VCのインスタンス生成
let sampleFullScreenVC = SampleFullScreenVC()
// 遷移時のデリゲートとして、独自定義したクラスをセット
sampleFullScreenVC.transitioningDelegate = SampleTransitioningDelegate()
// 表示。
// 上記デリゲートでセットした遷移動作をさせたいのでanimated は true にする必要アリ。
present(sampleFullScreenVC, animated: true)
2.フルスクリーン用VC
遷移のみに話を絞れば、特に何も書く必要がありません。
dismiss するときに animated: true
を忘れないように、くらいです。
3.UIViewControllerTransitioningDelegate
VC を present するとき、dismiss するときにどの
UIViewControllerAnimatedTransitioning
を使うかを決める。だけ。
1.インライン用VCのコード内で、フルスクリーン用VCの遷移時のデリゲートにセットしたのがこいつです。
class SampleTransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate {
// present するときに使うUIViewControllerAnimatedTransitioning
func animationController(
forPresented presented: UIViewController,
presenting: UIViewController,
source: UIViewController
) -> UIViewControllerAnimatedTransitioning? {
return SamplePresentAnimatedTransition()
}
// dismiss するときに使うUIViewControllerAnimatedTransitioning
func animationController(
forDismissed dismissed: UIViewController
) -> UIViewControllerAnimatedTransitioning? {
return SampleDismissAnimatedTransition()
}
}
4.UIViewControllerAnimatedTransitioning
遷移時のアニメーションなどを記述していくクラス。
上の UIViewControllerAnimatedTransitioning のコードにて、
present や dismiss するときにこれ使うよ〜と返却したのがこいつです。
class SamplePresentAnimatedTransition: NSObject, UIViewControllerAnimatedTransitioning {
// 遷移の秒数を定義。正直これは無視できるので、定義されてるな〜程度に。
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.5
}
// 遷移の具体的な動きをここに書いていく。遷移時にOS側から呼ばれるデリゲートメソッドです。
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// 略
// 遷移が完了したことを知らせるメソッドを最後に必ず呼ぶ(後述)
transitionContext.completeTransition(true)
}
}
以上4つです。
何となくイメージできたでしょうか。
再掲。
- インライン用VC: 今回遷移前のVC
- フルスクリーン用VC: 今回遷移後のVC
- UIViewControllerTransitioningDelegate: 遷移時にどの UIViewControllerAnimatedTransitioning (↓これ) を利用するかを定義する
- UIViewControllerAnimatedTransitioning: 遷移時の具体的なアニメーションを定義する
ではこれ以降、今回の実装に入っていきます!
フルスクリーン表示する
1.透明のフルスクリーン用VCをpresent する
インライン用VCにて以下のコードを書きます。
let sampleFullScreenVC = SampleFullScreenVC()
sampleFullScreenVC.view.backgroundColor = .clear
if let transition = SampleTransitioningDelegate() {
sampleFullScreenVC.transitioningDelegate = transition
}
present(sampleFullScreenVC, animated: true)
インライン用VCのコードは以上です。
以降は Transitioning 系やフルスクリーン用VCのコードのみになります。
2.UIViewControllerTransitioningDelegate
を定義する
上述のコードまんまですが。
class SampleTransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate {
func animationController(
forPresented presented: UIViewController,
presenting: UIViewController,
source: UIViewController
) -> UIViewControllerAnimatedTransitioning? {
return SamplePresentAnimatedTransition()
}
}
present のコードのみ書き、いまはdismiss を省略しています。UIViewControllerTransitioningDelegate
のコードもこれで以上です。
3.フルスクリーン用VCに見た目が変わらない位置に要素を配置する
ここが今回の肝で、遷移の具体的な動きを書いていきます。UIViewControllerAnimatedTransitioning
プロトコルに準拠した SamplePresentAnimatedTransition です。
長くなるので分割して書きます。
(最後のコードまとめにまとめて載せています)
まず、クラス宣言と遷移時間を定義します。
class SamplePresentAnimatedTransition: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.5
}
適当に0.5秒にしました。
次に、遷移アニメーションを記述していきます。
まず最初に、遷移前のVC(インライン用VC) と 遷移後のVC(フルスクリーン用VC) を取り出します。
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let from = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) as? UINavigationController,
let playerVC = from.topViewController as? SamplePlayerAndCollectionVC,
let to = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) as? SampleFullScreenVC else {
transitionContext.completeTransition(false)
return
}
今回インラインVC は UINavigationController 配下にあったので、UITransitionContextViewControllerKey.from
で取得できるのは UINavigationController となっています。
UINavigationController を使っていない場合はもちろん .from で直接遷移元のVC を取得できます。
次に、遷移時のコンテナビューに、フルスクリーン用VCのview を貼り付けます。
let containerView = transitionContext.containerView
containerView.addSubview(to.view)
唐突に出てきたコンテナビューて何やねん感🤔
コンテナビューとは、遷移時にiOSが自動で生成するUIViewです。
今回で言うと、下図のようにインラインVCの上に入ります。

では、このコンテナビューをどのように利用していくかですが、
遷移先のview を addSubview する以外は特に何も操作する必要はなさそうです。
遷移のビューなので、遷移中にコンテナビューにアニメーション用の要素を貼ってリッチに動かす、
的なこともしなくてよさそうです。
僕が試したところ、コンテナビュー自体の背景色をフェードインで黒くしようと思ったのですが、
いきなり黒になってしまい、うまくアニメーションしませんでした。
なので、フルスクリーン用VCの要素を操作してフェードインなどさせています🙌
さて次に、フルスクリーン用VCの上に、見た目が変わらない位置に要素を配置します。
guard let targetView = playerVC.targetView,
let originalSuperviewOfTargetView = targetView.superview else {
transitionContext.completeTransition(false)
return
}
let initialTargetViewFrame = to.view.convert(
targetView.frame,
from: targetView.superview
)
// この setup ではdismiss 時に使うためにフルスクリーン用VCのプロパティに保持しているだけ
to.setup(
targetView: targetView,
originalSuperviewOfTargetView: originalSuperviewOfTargetView,
initialTargetViewFrame: initialTargetViewFrame
)
let containerView = transitionContext.containerView
containerView.addSubview(to.view)
to.view.addSubview(targetView)
targetView.frame = initialTargetViewFrame
このあたりの動きは前回の記事 1.透明のフルスクリーン用VCを表示し、見た目が変わらない位置に要素を配置する
でもう少し詳しく説明しています。
4.背景を黒くし、遷移完了のメソッドを呼ぶ
UIView.animate(
withDuration: transitionDuration(using: transitionContext),
animations: {
to.view.backgroundColor = .black
},
completion: { _ in
// 遷移が完了したタイミングで必ず呼ぶ
transitionContext.completeTransition(true)
// 端末を回転させる
to.rotateToLandscape()
}
)
アニメーションで背景を黒くしたあとで
遷移完了を知らせるメソッド transitionContext.completeTransition(_ didComplete: Bool)
を呼んでいます。
これは遷移が完了したタイミングで必ず呼びます。
呼ばない場合、以降も遷移中のままとしてiOSに扱われ、遷移中に実行できる挙動しか受け付けなくなりそうです。
上の例で言うと、それを呼んだあとで端末を回転させています。
試したところ、完了メソッドを呼ぶ前に端末回転のコードを書いても、呼んだ後で回転しました。
遷移中は画面回転が許可されていないと思われ、
つまり完了メソッドを呼び忘れた場合、それ以降画面回転が効かなくなりそうです。
画面回転は「遷移」の範囲外の挙動なのでしょうね🤔
これで UIViewControllerAnimatedTransitioning
の実装は完了です。
5.全画面表示にする
func rotateToLandscape() {
UIDevice.current.setValue(
UIInterfaceOrientation.landscapeRight.rawValue,
forKey: "orientation"
)
UIView.animate(
withDuration: 0.5,
animations: {
if let superviewFrame = self.targetView?.superview?.frame {
self.targetView?.frame = CGRect(
x: 0.0,
y: 0.0,
width: superviewFrame.width,
height: superviewFrame.height
)
}
self.view.layoutIfNeeded()
}
)
}
フルスクリーン用VCに書きます。
こちらも前回の記事の 3.画面を強制的に横向きにしつつ、要素を全画面に表示する で
もう少しだけ詳しく説明しています。
以上でインラインVCからフルスクリーンVCへの遷移は完了です!
dismiss に関しては present の逆回しに考えていけばよいだけなので、
(本ポストも長くなってしまったので)割愛します。
以下にコードまとめを載せておきます。
dismiss の分も載せています。
コードまとめ
1.インライン用VC
class SamplePlayerAndCollectionVC: UIViewController {
override var shouldAutorotate: Bool {
return false
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .portrait
}
func openFullScreen() {
let sampleFullScreenVC = self.storyboard?.instantiateViewController(withIdentifier: "SampleFullScreenVC") as! SampleFullScreenVC
sampleFullScreenVC.view.backgroundColor = .clear
sampleFullScreenVC.transitioningDelegate = SampleTransitioningDelegate()
present(sampleFullScreenVC, animated: true)
}
}
2.フルスクリーン用VC
class SampleFullScreenVC: UIViewController {
private(set) var targetView: UIView?
private(set) var originalSuperviewOfTargetView: UIView?
private(set) var initialTargetViewFrame: CGRect?
override var shouldAutorotate: Bool {
return true
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .allButUpsideDown
}
func setup(
targetView: UIView?,
originalSuperviewOfTargetView: UIView?,
initialTargetViewFrame: CGRect?
) {
self.targetView = targetView
self.originalSuperviewOfTargetView = originalSuperviewOfTargetView
self.initialTargetViewFrame = initialTargetViewFrame
}
func rotateToLandscape() {
UIDevice.current.setValue(
UIInterfaceOrientation.landscapeRight.rawValue,
forKey: "orientation"
)
UIView.animate(
withDuration: 0.5,
animations: {
if let superviewFrame = self.targetView?.superview?.frame {
self.targetView?.frame = CGRect(
x: 0.0,
y: 0.0,
width: superviewFrame.width,
height: superviewFrame.height
)
}
self.view.layoutIfNeeded()
}
)
}
func rotateToPortrait(
completion: (() -> ())?
) {
UIDevice.current.setValue(
UIInterfaceOrientation.portrait.rawValue,
forKey: "orientation"
)
UIView.animate(
withDuration: 0.5,
animations: {
if let initialTargetViewFrame = self.initialTargetViewFrame {
self.targetView?.frame = CGRect(
x: initialTargetViewFrame.minX,
y: initialTargetViewFrame.minY,
width: initialTargetViewFrame.width,
height: initialTargetViewFrame.height
)
}
self.view.layoutIfNeeded()
},
completion: { _ in
completion?()
}
)
}
// フルスクリーンをdismiss するメソッド。どこからか呼ぶ必要あり
func animateToClose() {
rotateToPortrait { [weak self] in
self?.dismiss(animated: true)
}
}
}
3.UIViewControllerTransitioningDelegate
class SampleTransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate {
func animationController(
forPresented presented: UIViewController,
presenting: UIViewController,
source: UIViewController
) -> UIViewControllerAnimatedTransitioning? {
return SamplePresentAnimatedTransition()
}
func animationController(
forDismissed dismissed: UIViewController
) -> UIViewControllerAnimatedTransitioning? {
return SampleDismissAnimatedTransition()
}
}
4.UIViewControllerAnimatedTransitioning
class SamplePresentAnimatedTransition: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.5
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let from = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) as? UINavigationController,
let playerVC = from.topViewController as? SamplePlayerAndCollectionVC,
let to = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) as? SampleFullScreenVC else {
transitionContext.completeTransition(false)
return
}
let containerView = transitionContext.containerView
containerView.addSubview(to.view)
guard let targetView = playerVC.targetView,
let originalSuperviewOfTargetView = targetView.superview else {
transitionContext.completeTransition(false)
return
}
let initialTargetViewFrame = to.view.convert(
targetView.frame,
from: targetView.superview
)
to.setup(
targetView: targetView,
originalSuperviewOfTargetView: originalSuperviewOfTargetView,
initialTargetViewFrame: initialTargetViewFrame
)
to.view.addSubview(targetView)
targetView.frame = initialTargetViewFrame
UIView.animate(
withDuration: transitionDuration(using: transitionContext),
animations: {
to.view.backgroundColor = .black
},
completion: { _ in
transitionContext.completeTransition(true)
to.rotateToLandscape()
}
)
}
}
class SampleDismissAnimatedTransition: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.5
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let from = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) as? SampleFullScreenVC else {
transitionContext.completeTransition(false)
return
}
UIView.animate(
withDuration: transitionDuration(using: transitionContext),
animations: {
from.view.backgroundColor = .clear
},
completion: { _ in
if let targetView = from.targetView {
from.originalSuperviewOfTargetView?.addSubview(targetView)
if let initialTargetViewFrame = from.initialTargetViewFrame {
let frame = from.view.convert(
initialTargetViewFrame,
to: targetView.superview
)
targetView.frame = frame
}
}
transitionContext.completeTransition(true)
}
)
}
}
最後に
長くなってしまいましたが以上です。
お疲れ様でした!