【SwiftUI】ScenePhaseの使い方とSceneDelegateとの比較

SwiftUI

現在のSwiftUIの標準ではScenePhaseを使ってSceneの変化を検知します。
SceneDelegateと比較すると随分シンプルになりました。

active、inactive、backgroundの動作確認をするようなコードで紹介されている場合が殆どなので、
もう一歩踏み込んでSceneDelegateと比較しながら紹介したいと思います。

ScenePhaseの基本

サンプルコード

はじめにHelloWorldにScenePhaseを追加したコードを見ていきましょう。

struct ContentView: View {
    
    @Environment(\.scenePhase) private var scenePhase
    
    var body: some View {
        Text("Hello, world!")
            .padding()
            .onChange(of: scenePhase) { phase in
                switch phase {
                case .active:
                    print("active")
                case .inactive:
                    print("inactive")
                case .background:
                    print("background")
                @unknown default:
                    print("@unknown")
                }
            }
    }
}

任意のViewに@EnvironmentでscenePhaseの変数を作り、
任意のViewにonChange Modifierを実装します。
これでアプリの状態が遷移した場合にonChangeが呼ばれ、
scenePhaseの値に応じた処理をする事が出来ます。

複数のViewに分散して書くこともでき、該当Viewが存在すればそれぞれonChangeが処理されます。

ScenePhaseの値について

ScenePhaseは以下の3種の状態を持ちます。

ScenePhase状態
.activeアプリがフォアグラウンドで動作している状態
.inactiveアプリがactiveでもbackgroundでもない状態
.backgroundアプリがバックグラウンドにある状態

.inactiveが微妙な説明になってしまっているので補足します。

アプリがフォアグラウンドやバックグラウンドから離れる際に.inactiveになります。
ただし、フォアグラウンドからスイッチャーに入った場合は.inactiveになりますが、
バックグラウンドからスイッチャーに入っても.inactiveになりません。

大量のアプリがバックグラウンドにあって.inactiveになると大量の処理が入る可能性がある為、
当然の対応ではありますがフォアグラウンドからと違う点には気をつけましょう。

以下は遷移時のScenePhaseの変化になります。

遷移前遷移後ScenePhase
フォアグラウンドバックグラウンド.inactive
.background
フォアグラウンドスイッチャー.inactive
スイッチャーフォアグラウンド.active
スイッチャーバックグラウンド.background
バックグラウンドフォアグラウンド.inactive
.active
バックグラウンドスイッチャーなし
起動.active
※短時間に起動と終了を繰り返すと呼ばれない場合あり
スイッチャー終了.background

起動時のactiveは呼ばれない場合がありましたので、
起動時処理はScenePhaseは使用せずイニシャライザで行うようにしましょう。

.inactiveの判別について

ScenePhaseはシンプルに3つの状態しか持ちません。
その為phaseだけ見るとactiveとbackgroundのどちらからinactiveに遷移したかは一見判別出来ない様に見えます。

では直前の状態を保持する変数が必要かというと、そんなことはありません。
onChangeで自動生成したクロージャの引数名を思い出して下さい。
phaseの部分はnewValueでした。

//サンプルコードではnewValueをphaseに書き換えていた。
//Apple公式のドキュメント準拠。
.onChange(of: scenePhase) { newValue in 
    code
}

ではoldValueはどこかというと、onChangeの時点ではまだscenePhaseが変わっていません。
その為、inactiveの場合はscenePhaseがactiveかbackgroundかで
どちらから遷移したか判断する事が出来ます。

struct ContentView: View {
    
    @Environment(\.scenePhase) private var scenePhase
    
    var body: some View {
        Text("Hello, world!")
            .padding()
            .onChange(of: scenePhase) { phase in
                switch phase {
                case .active:
                    print("active")
                case .inactive:
                    switch scenePhase {
                    case .active:
                        print("active to inactive")
                    case .background:
                        print("background to inactive")
                    default:
                        print("default")
                    }
                case .background:
                    print("background")
                @unknown default:
                    print("@unknown")
                }
            }
    }
}

SceneDelegateとの比較

以前はSceneの遷移の応じた処理するにはSceneDelegateを使用していました。
ScenePhaseで大きく変わったので比較していきます。

なお今回はあくまでScenePhaseへの移行を想定した比較であり、
SceneDelegateのrootViewControllerやUIScene関連は複雑なるため触れません。
この辺りは目的毎に別な記事を書く予定です。

実装箇所について

SceneDelegateはAppDelegateに渡す為1つのクラスにまとめる必要がありました。
対してScenePhaseは複数のViewに分散して書く事が出来ますし、
1カ所に集約したい場合はContentViewにまとめて書く事も出来ます。
分散させるか集約するかが選べる様になり自由度が上がりました。

また、SceneDelegateはデータをSceneDelegateクラスに渡す方法を意識しなければなりませんでしたが、
ScenePhaseはView内から直接データを扱える為、より直感的に実装を行えます。

SceneDelegateメソッドとScenePhaseの値について

SceneDelegateにはSceneの変化に関するメソッドが6個ありましたが、ScenePhaseの状態は3個しかありません。
どのメソッドとどの状態が対応しているか、または代用方法の有無について紹介して行きます。

状態UISceneDelegateScenePhase備考
シーンの接続sceneなしContentViewのinit等で代用
シーンの切断sceneDidDisconnectなし代用方法なし
inactive
(backgroundから)
sceneWillEnterForegroundphase == .inactive
scenePhase == .background
phase → newValue
scenePhase → Environment Value
activesceneDidBecomeActivephase == .active
inactive
(activeから)
sceneWillResignActivephase == .inactive
scenePhase == .active
backgroundsceneDidEnterBackgroundphase == .background

ScenePhaseの説明でも書きましたが、inactiveはonChangeのphase(newValue)とscenePhaseを組み合わせることで判別出来ます。
これによってUISceneDelegateのメソッドに対応する事が出来ます。

シーンの接続に関しては基本的にはContentViewのinit等など、
シーンの接続時に1度だけ実行される箇所で代用可能です。

シーンの切断sceneDidDisconnectだけは代用方法がありません。
これが欲しい場合はSceneDelegateを実装するしかありません。
バックグラウンドの置いてあるアプリの管理はOSに任せるしかなく、
シーンの切断やアプリの終了は確実に実行される訳ではないので、
使って欲しくないということではないかと思っています。

その他SceneDelegateのメソッドについて

SceneDelegateには他にもSceneの接続に関するメソッドがあります。
そちらに関してはScenePhaseとは別で用意されています。
UserActivity関連もOpenURL関連もそれぞれModifierで用意されています。

コメント

タイトルとURLをコピーしました