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クラスを作成します。
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の設定を行います。
<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への移行は、一見複雑に見えますが、以下のポイントを押さえればスムーズに進められます:
- iOS 27以降では必須対応であることを理解する(最重要)
- コールド起動とホット起動の違いを理解する
- connectionOptionsを正しく処理する
- 責務を適切に分離する(App Delegate vs Scene Delegate)
- 十分なテストを行う
特にコールド起動時のURL処理(Universal LinksとCustom URL Scheme)は見落としがちなので、必ず実装とテストを行いましょう。
Scene-basedライフサイクルに移行することで、iOS 27以降でもアプリが正常に動作し、将来的なiOSのアップデートにも対応しやすくなり、マルチウィンドウ対応などの機能追加もスムーズに行えるようになります。
参考資料
- Apple - UISceneDelegate
- Apple TN3187: Migrating to the UIKit scene-based life cycle
- WWDC 2019 - Architecting Your App for Multiple Windows
この記事が、UIScene Delegateへの移行を検討している方の参考になれば幸いです!
Discussion