[iOS] ViewController を切り替えていい感じにフルスクリーン表示する

前説

こんな感じでいい感じに動画コンテンツみたくインライン表示、フルスクリーン表示を切り替えたい。

ViewController を切り替えていい感じにフルスクリーン表示する

前提条件

Xcode: 12.1
シミュレータ: iOS14系

というわけで作っていきます。

画面構成

画面構成

流れをシーケンス図で表示するとこうなる

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

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

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

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

前準備1. それぞれのVCの回転制御について

今回の前提として、インライン用VCは回転不可で縦方向のみサポート。

    override var shouldAutorotate: Bool {
        return false
    }
    override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        return .portrait
    }

フルスクリーン用VCは回転可能で縦・横両方サポートとします。

    override var shouldAutorotate: Bool {
        return true
    }
    override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        return .allButUpsideDown
    }

前準備2. インライン用VCにおける、対象の要素についてのView構成

今回はインライン用VCにある要素を、そのままフルスクリーン用VCに渡して全画面表示にします。
なのでフルスクリーン表示時には、インライン用VCからその要素がなくなっている状態のですが、
その間も画面が崩れないようにする必要があります。

そのため、インライン用VCにおいては、対象の要素の親のUIView を一つ作り、
表示崩れしないように領域を確保しておきます。

インライン用VCのビューヒエラルキー

では以下からいい感じにフルスクリーン表示する処理を記述していきます!
段階を追ってコードを細切れに書いていますが、最下部には必要なコードをまとめています。

フルスクリーン表示にするとき

1.透明のフルスクリーン用VCを表示し、見た目が変わらない位置に要素を配置する

まずインライン 表示用VCにてタップイベントなど適当なイベントのハンドラを作り、
そこをフルスクリーン表示用処理の入り口とします。
ここは今回の内容とは関係ないので割愛します。

処理を開始したらまず透明のフルスクリーン用VCを作成します。

        let sampleFullScreenVC = self.storyboard?.instantiateViewController(withIdentifier: "SampleFullScreenVC") as! SampleFullScreenVC
        sampleFullScreenVC.view.backgroundColor = .clear

フルスクリーン用VCに要素を渡し、後の位置調整やアニメーションの処理はそちらのVCで行うことにします。
フルスクリーン用VCに func animateToFullScreen(_ targetView: UIView) というメソッドを用意しました。

        present(sampleFullScreenVC, animated: false) { [weak self] in
            guard let self = self else {
                return
            }
            
            if let view = self.targetView {
                sampleFullScreenVC.animateToFullScreen(view)
            }
        }

インライン用VCで必要なコードは以上です。
これ以降インライン -> フルスクリーン、フルスクリーン -> インライン の処理はすべて
フルスクリーン用VCに書いていきます。

さて、インライン用VCから要素が渡されたので、まずは見た目上の位置が変わらないよう
フルスクリーン用VC に貼り付けます。

    func animateToFullScreen(_ targetView: UIView) {
        // フルスクリーン -> インライン に戻るときに操作するので、要素をプロパティに保持する
        self.targetView = targetView
        
        // フルスクリーン -> インライン で元いたView に貼り戻すため、元々の親View をプロパティに保持する
        self.originalSuperviewOfTargetView = targetView.superview
        
        // 貼り付けたときの見た目上の位置が変わらないよう、frame を変換する
        // また、フルスクリーン -> インライン で元の位置に戻すのに必要なため、
        // 最初の位置を frame としてプロパティに保持しておく
        self.initialTargetViewFrame = self.view.convert(
            targetView.frame,
            from: targetView.superview
        )
        if let frame = self.initialTargetViewFrame {
            targetView.frame = frame
        }
            
        self.view.addSubview(targetView)

上のコメントにもあるようにフルスクリーン -> インラインに戻るときに、
元の位置や、貼り戻す親のUIView が必要なため、以下のようにプロパティを用意します。

    private var targetView: UIView?
    private var originalSuperviewOfTargetView: UIView?
    private var initialTargetViewFrame: CGRect?

以上で、見た目は変わらないままで、要素をインラインVCからフルスクリーンVCに引き渡せました。

2.フルスクリーン用VCの背景色を黒くする

いまの段階ではフルスクリーン用VCの背景色が透明なため、
奥にあるインライン用VCが透けて見えています。
なので背景色を黒くするのですが、急に真っ暗になるとびっくりするので
アニメーションで徐々に黒くしていきます。

        UIView.animate(
            withDuration: 0.5,
            animations: {
                self.view.backgroundColor = .black
            },
            completion: { _ in
                // この後、このcompletion 内で画面横向き & 全画面表示の処理をします
            }
        )

上のコードでは背景色が黒くなるのを待ってから、横向き & 全画面表示の処理を実行しようとしています。
なぜかと言うと、背景色を変えるのと同時に横向きにすると、
奥にいるインラインVCも横向きになってしまうのが少し透けて見えてしまうからです。

以下のように見えてしまい、かっこ悪いですね!

背景が崩れて見えてかっこ悪い

3.画面を強制的に横向きにしつつ、要素を全画面に表示する

あとは以下のコードを、上記UIView.animate メソッドの completion 内に書けばOKです。

                // 端末を強制的に横向きにする
                UIDevice.current.setValue(
                    UIInterfaceOrientation.landscapeRight.rawValue,
                    forKey: "orientation"
                )
                
                // 要素を全画面にする
                UIView.animate(
                    withDuration: 0.5,
                    animations: {
                        if let superviewFrame = targetView.superview?.frame {
                            targetView.frame = CGRect(
                                x: 0.0,
                                y: 0.0,
                                width: superviewFrame.width,
                                height: superviewFrame.height
                            )
                        }
                        
                        self.view.layoutIfNeeded()
                    }
                )

以上でインライン表示 -> フルスクリーン表示の遷移は完了です!

一点注意として、要素に対してframe で位置や大きさを指定していますが、
これだと回転させると表示崩れを起こしてしまうかもしれません。
なのでAutoLayout で指定した方がいいとは思いますが、今回の主旨とは関係ないので割愛しています。

以降フルスクリーン表示 -> インライン表示に戻る時の処理も書きますが、
ここで書いた処理を逆にすればよいだけです。

インライン表示に戻るとき

1.画面を強制的に縦向きに戻しつつ、見た目上の元の位置に要素を戻す

以下のコードを書きます。

        // 端末を強制的に縦向きに戻す
        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 内でフルスクリーン用VCの背景色を透明にします
            }
        )

元の位置に戻すために、フルスクリーン表示時の最初に保存しておいた frame を利用しています。

2.フルスクリーン用VCの背景色を透明にする

以下のコードを書きます。
特筆することはありません。

                UIView.animate(
                    withDuration: 0.5,
                    animations: { [weak self] in
                        self?.view.backgroundColor = .clear
                    },
                    completion: { _ in
                        // この後、このcompletion 内で要素をインライン用VCに貼り直し、フルスクリーン用VCを消します
                    }
                )

3.要素をインライン用VCに貼り直し、フルスクリーン用VCを消す

以下のコードを書きます。
こちらも特筆することはありません。

                        if let targetView = self.targetView {
                            // インライン用VCの元いた親View に貼り戻す
                            self.originalSuperviewOfTargetView?.addSubview(targetView)

                            // 貼り付けたときの見た目上の位置が変わらないよう、frame を変換する
                            if let initialTargetViewFrame = self.initialTargetViewFrame {
                                let frame = self.view.convert(
                                    initialTargetViewFrame,
                                    to: targetView.superview
                                )
                                targetView.frame = frame
                            }
                        }
                        
                        self.dismiss(animated: false)

以上でできました!

コードまとめ

インライン用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
        
        present(sampleFullScreenVC, animated: false) { [weak self] in
            guard let self = self else {
                return
            }
            
            if let view = self.targetView {
                sampleFullScreenVC.animateToFullScreen(view)
            }
        }
    }
}

フルスクリーン用VC

class SampleFullScreenVC: UIViewController {
    private var targetView: UIView?
    private var originalSuperviewOfTargetView: UIView?
    private var initialTargetViewFrame: CGRect?
    
    override var shouldAutorotate: Bool {
        return true
    }
    override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        return .allButUpsideDown
    }
    
    // インライン -> フルスクリーン
    func animateToFullScreen(_ targetView: UIView) {
        self.targetView = targetView
        self.originalSuperviewOfTargetView = targetView.superview
        self.initialTargetViewFrame = self.view.convert(
            targetView.frame,
            from: targetView.superview
        )
        if let frame = self.initialTargetViewFrame {
            targetView.frame = frame
        }
            
        self.view.addSubview(targetView)
        
        UIView.animate(
            withDuration: 0.5,
            animations: {
                self.view.backgroundColor = .black
            },
            completion: { _ in
                UIDevice.current.setValue(
                    UIInterfaceOrientation.landscapeRight.rawValue,
                    forKey: "orientation"
                )

                UIView.animate(
                    withDuration: 0.5,
                    animations: {
                        if let superviewFrame = targetView.superview?.frame {
                            targetView.frame = CGRect(
                                x: 0.0,
                                y: 0.0,
                                width: superviewFrame.width,
                                height: superviewFrame.height
                            )
                        }
                        self.view.layoutIfNeeded()
                    }
                )
            }
        )
    }
    
    // フルスクリーン -> インライン
    func animateToInline() {
        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
                UIView.animate(
                    withDuration: 0.5,
                    animations: { [weak self] in
                        self?.view.backgroundColor = .clear
                    },
                    completion: { _ in
                        if let targetView = self.targetView {
                            self.originalSuperviewOfTargetView?.addSubview(targetView)
                            
                            if let initialTargetViewFrame = self.initialTargetViewFrame {
                                let frame = self.view.convert(
                                    initialTargetViewFrame,
                                    to: targetView.superview
                                )
                                targetView.frame = frame
                            }
                        }
                        
                        self.dismiss(animated: false)
                    }
                )
            }
        )
    }
}

最後に

長くなってしまいましたが、段階を追っていけばシンプルな手順かなと思います。

以上です。お疲れ様でした!

[iOS] ViewController を切り替えていい感じにフルスクリーン表示する」への1件のフィードバック

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中

%d人のブロガーが「いいね」をつけました。