🍎

UIScene Delegateベースのライフサイクルに移行した話

に公開

はじめに

iOS 13でUIScene Delegateが導入されて以来、Appleは従来のApp DelegateベースのライフサイクルからScene-basedライフサイクルへの移行を推奨してきました。そしてiOS 27からは、この移行が必須となりました。

本記事では、実際のプロダクションアプリでこの移行を行った際の手順と、特にハマりやすいポイントについて解説します。

背景と動機

なぜ移行するのか?

🚨 最重要: iOS 27以降の必須対応

iOS 27(2026年秋頃予定)からは、UIScene Delegateに対応していないアプリは起動できなくなります。

AppleはTN3187で以下のように明言しています:

In the next major release following iOS 26, UIScene lifecycle will be required when building with the latest SDK; otherwise, your app won’t launch.

iOS26の間に、UIScene Delegate対応は必須です。対応しないとアプリが起動しません。

その他のメリット

  • Appleの推奨: iOS 13以降、UIScene Delegateが標準となっている
  • マルチウィンドウ対応: iPadでの複数ウィンドウ表示に対応可能
  • ライフサイクルの明確化: アプリ全体とシーン(画面)のライフサイクルが分離され、責務が明確になる
  • 将来性: 新しいiOS機能への対応がしやすくなる

移行前の構成

  • iOS 13以前のApp Delegateベースのアーキテクチャ
  • AppDelegateで全てのライフサイクル管理

移行手順

1. SceneDelegateクラスの作成

まず、UIWindowSceneDelegateプロトコルに準拠したSceneDelegateクラスを作成します。

SceneDelegate.swift
import SwiftUI
import LineSDK

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    
    var window: UIWindow?
    
    func scene(
        _ scene: UIScene,
        willConnectTo session: UISceneSession,
        options connectionOptions: UIScene.ConnectionOptions
    ) {
        guard let windowScene = scene as? UIWindowScene else { return }
        
        let window = UIWindow(windowScene: windowScene)
        window.rootViewController = MainViewController()
        window.makeKeyAndVisible()
        self.window = window
    }
    
    // その他のライフサイクルメソッド...
}

2. Info.plistの設定

UIApplicationSceneManifestを追加して、Scene Delegateの設定を行います。

Info.plist
<key>UIApplicationSceneManifest</key>
<dict>
    <key>UIApplicationSupportsMultipleScenes</key>
    <false/>
    <key>UISceneConfigurations</key>
    <dict>
        <key>UIWindowSceneSessionRoleApplication</key>
        <array>
            <dict>
                <key>UISceneConfigurationName</key>
                <string>Default Configuration</string>
                <key>UISceneDelegateClassName</key>
                <string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
            </dict>
        </array>
    </dict>
</dict>

ポイント:

  • UIApplicationSupportsMultipleScenesは、マルチウィンドウが不要な場合はfalse
  • UISceneDelegateClassName$(PRODUCT_MODULE_NAME).SceneDelegate形式で指定
  • SwiftUIベースのスプラッシュ画面を使う場合は、UISceneStoryboardFileの指定は不要

3. Window初期化の移行

AppDelegateからSceneDelegateにwindow関連のコードを移行します。

移行前(AppDelegate):

class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    
    func application(_ application: UIApplication, 
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        setScreen()
        return true
    }
    
    private func setScreen() {
        let window = UIWindow(frame: UIScreen.main.bounds)
        window.rootViewController = MainViewController()
        window.makeKeyAndVisible()
        self.window = window
    }
}

移行後(SceneDelegate):

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?
    
    func scene(_ scene: UIScene, 
               willConnectTo session: UISceneSession,
               options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = scene as? UIWindowScene else { return }
        
        let window = UIWindow(windowScene: windowScene)
        window.rootViewController = MainViewController()
        window.makeKeyAndVisible()
        self.window = window
    }
}

4. DeepLinkを使っている場合の注意点:コールド起動とホット起動

DeepLinkUniversal Links、Custom URL Scheme)を使っている場合、SceneDelegateではコールド起動とホット起動の違いに注意する必要があります。

コールド起動とホット起動とは?

コールド起動:アプリが停止している状態からの起動
ホット起動:アプリが既に起動している状態での起動

AppDelegateでは違いを意識する必要はありませんでしたが、SceneDelegateでは呼ばれるメソッドが異なります

起動状態による違い

起動状態 呼ばれるメソッド URL取得元
コールド起動
(アプリ停止時)
scene(_:willConnectTo:options:) connectionOptions
ホット起動
(アプリ起動中)
scene(_:openURLContexts:)
scene(_:continue:)
メソッドのパラメータ

この違いを理解していないと、「アプリ起動中は動くのに停止している状態だと」という問題に遭遇します。

5. Universal Linksの移行

Universal Linksの処理をSceneDelegateに移行します。

移行前(AppDelegate):

func application(_ application: UIApplication, 
                 continue userActivity: NSUserActivity,
                 restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    // Universal Links処理
    return true
}

移行後(SceneDelegate):

// ホット起動:アプリ起動中に呼ばれる
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
    handleUniversalLink(userActivity: userActivity)
}

private func handleUniversalLink(userActivity: NSUserActivity) {
    guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
          let url = userActivity.webpageURL else {
        return
    }
    // Universal Links処理
}

コールド起動の対応:

func scene(_ scene: UIScene, 
           willConnectTo session: UISceneSession,
           options connectionOptions: UIScene.ConnectionOptions) {
    // ... window初期化 ...
    
    // コールド起動:アプリ停止状態から起動した場合
    if let userActivity = connectionOptions.userActivities.first {
        handleUniversalLink(userActivity: userActivity)
    }
}

6. Custom URL Schemeの移行

Custom URL Schemeの処理をSceneDelegateに移行します。

移行前(AppDelegate):

func application(_ application: UIApplication, 
                 open url: URL, 
                 options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
    // Custom URL Scheme処理
    return true
}

移行後(SceneDelegate):

// ホット起動:アプリ起動中に呼ばれる
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
    guard let url = URLContexts.first?.url else { return }
    handleURLScheme(url: url)
}

🔥 ハマりどころ: コールド起動の対応漏れ

scene(_:openURLContexts:)アプリが既に起動している時のみ呼ばれます。

アプリが停止している状態(コールド起動)からCustom URL Schemeで起動した場合、このメソッドは呼ばれません!

解決方法:

scene(_:willConnectTo:options:)connectionOptions.urlContextsもチェックする必要があります。

完全な実装例:

func scene(_ scene: UIScene, 
           willConnectTo session: UISceneSession,
           options connectionOptions: UIScene.ConnectionOptions) {
    guard let windowScene = scene as? UIWindowScene else { return }
    
    let window = UIWindow(windowScene: windowScene)
    window.rootViewController = MainViewController()
    window.makeKeyAndVisible()
    self.window = window
    
    // コールド起動:URL Scheme処理
    if let urlContext = connectionOptions.urlContexts.first {
        handleURLScheme(url: urlContext.url)
    }
    
    // コールド起動:Universal Links処理
    if let userActivity = connectionOptions.userActivities.first {
        handleUniversalLink(userActivity: userActivity)
    }
}

// ホット起動:URL Scheme処理
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
    guard let url = URLContexts.first?.url else { return }
    handleURLScheme(url: url)
}

// ホット起動:Universal Links処理
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
    handleUniversalLink(userActivity: userActivity)
}

7. ライフサイクルメソッドの移行

アプリのライフサイクルメソッドも移行が必要です。

対応表:

AppDelegate SceneDelegate
applicationDidBecomeActive sceneDidBecomeActive
applicationWillResignActive sceneWillResignActive
applicationDidEnterBackground sceneDidEnterBackground
applicationWillEnterForeground sceneWillEnterForeground

8. AppDelegateに残すもの

すべてをScene Delegateに移行するわけではありません。以下はApp Delegateに残します:

  • アプリ全体の初期化: Firebase、SDK初期化など
  • プッシュ通知登録: デバイストークンの取得

テスト項目

移行後、以下の項目をテストすることをお勧めします:

基本動作

  • アプリの通常起動
  • フォアグラウンド/バックグラウンド遷移
  • アプリ強制終了後の再起動

URL処理(コールド起動 = アプリ停止時)⚠️ 重要

※必ずタスクスイッチャーからアプリを削除してからテストすること

  • Custom URL Schemeでのアプリ起動(完全終了状態から)
  • Universal Linksでのアプリ起動(完全終了状態から)
  • プッシュ通知からのDeepLink起動(完全終了状態から)

URL処理(ホット起動 = アプリ起動中)

※アプリをバックグラウンドに移動してからテストすること

  • アプリ起動中のCustom URL Scheme
  • アプリ起動中のUniversal Links
  • バックグラウンドからのURL起動

その他

  • ライフサイクルイベントの動作確認
  • 既存機能の回帰テスト

💡 テストのコツ:

  • コールド起動: アプリをタスクスイッチャーから削除(上にスワイプ)して完全に終了させる
  • ホット起動: ホームボタン(またはスワイプジェスチャー)でアプリをバックグラウンドに移動させる
  • 両方のパターンで必ずテストすることが重要

まとめ

iOS 27以降ではUIScene Delegate対応が必須となり、対応していないアプリは起動できなくなります。そのため、できるだけ早めの対応が推奨されます。

UIScene Delegateへの移行は、一見複雑に見えますが、以下のポイントを押さえればスムーズに進められます:

  1. iOS 27以降では必須対応であることを理解する(最重要)
  2. コールド起動とホット起動の違いを理解する
  3. connectionOptionsを正しく処理する
  4. 責務を適切に分離する(App Delegate vs Scene Delegate)
  5. 十分なテストを行う

特にコールド起動時のURL処理(Universal LinksとCustom URL Scheme)は見落としがちなので、必ず実装とテストを行いましょう。

Scene-basedライフサイクルに移行することで、iOS 27以降でもアプリが正常に動作し、将来的なiOSのアップデートにも対応しやすくなり、マルチウィンドウ対応などの機能追加もスムーズに行えるようになります。

参考資料


この記事が、UIScene Delegateへの移行を検討している方の参考になれば幸いです!

株式会社アルカディット テックブログ

Discussion