はじめに
iOS13でSceneDelegateが導入されて久しいですが、
この度既存プロジェクトにSceneDelegateを導入する必要があったので、
AppDelegateに書かれていた処理を適宜SceneDelegateに書きScene対応しました。
今回は以下の起動経路に対応する必要があったので、それについて記載しています。
起動経路 | アプリの状態 |
---|---|
リモートプッシュ通知 | アプリ未起動時に受信 フォアグラウンド時に受信 バックグラウンド時に受信 |
URLスキームによる起動 | アプリ未起動時に受信 バックグラウンド時に受信 |
Quick Action(3D Touch) | アプリ未起動時に受信 バックグラウンド時に受信 |
Spotlight検索 | アプリ未起動時に受信 バックグラウンド時に受信 |
SceneDelegateとは
iOS13からUIScene
というクラスが追加され、
マルチウィンドウに対応できるようになりました。
iOS12まではアプリのプロセス(起動や終了などを指す)は1つで、
それに対するUIインスタンスも1つでした。
これがiOS13からはアプリのプロセスは1つで、
それに対するUIインスタンスは複数になりました。
このUIインスタンスをSceneといいます。
これまではアプリのプロセス、UIの状態管理を全てAppDelegateで行っていましたが、
このSceneの概念が登場したことに伴い、
AppDelegateではアプリ全体のプロセスのみを管理して、
新たに追加されたSceneDelegateで画面の表示などのUIの状態やライフサイクル管理を担うようになりました。
詳細は[iOS13] UIScene APIを使用する [Xcode11]の記事が大変参考になりました。
SceneDelegateのライフサイクル
【xcode 11】新たに導入されたsceneDelegateの各メソッドが呼ばれるタイミングの記事が具体的でわかりやすかったです。
実装ベースでライフサイクルのイメージを掴む事ができると思います。
導入の経緯
Widgetを入れるためにその前段としてSceneDelegateを導入しました。
実装
前提
- サポート対象: iOS10以降
- iOS13の前後で通知先がAppDelegateとSceneDelegateで分かれるため、両方実装しています。
- リポートプッシュ通知はFirebase Cloud Messagingを利用しています
- windowの作成やInfo.plistの設定などは割愛します
リモートプッシュ通知(Firebase Cloud Messaging)
各通知先
起動方法 | 通知先(~iOS12) | 通知先(iOS13~) |
---|---|---|
アプリ未起動時に受信 | ①application(_:didFinishLaunchingWithOptions:) -> Bool
|
④ func scene(_:willConnectTo:options:) |
フォアグラウンド時に受信 | ② UNUserNotificationCenterDelegateの準拠先に通知 func userNotificationCenter(_:didReceive: withCompletionHandler) |
⑤ UNUserNotificationCenterDelegateの準拠先に通知 func userNotificationCenter(_:didReceive: withCompletionHandler) |
バックグラウンド時に受信 | ③ 同上 |
⑥ 同上 |
実装
extension AppDelegate: UIApplicationDelegate, UIResponder {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
if #available(iOS 13, *) {} else {
// iOS12以下はAppDelegateをdelegate先に設定
UNUserNotificationCenter.current().delegate = self
}
let userInfo = launchOptions?[UIApplication.LaunchOptionsKey.remoteNotification] as? [AnyHashable: Any]
// ① userInfoから特定のURLSchemeを埋め込んだkeyを取得して画面遷移など使用
return true
}
}
// MARK: - UNUserNotificationCenterDelegate
extension AppDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
let userInfo = response.notification.request.content.userInfo
// ②③ userInfoから特定のURLSchemeを埋め込んだkeyを取得して画面遷移など使用
completionHandler()
}
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler([.alert, .badge, .sound])
}
}
@available(iOS 13.0, *)
extension SceneDelegate: UIResponder, UIWindowSceneDelegate {
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// iOS13以上はSceneDelegateをdelegate先に設定
UNUserNotificationCenter.current().delegate = self
// プッシュ通知による起動
if let response = connectionOptions.notificationResponse {
let userInfo = response.notification.request.content.userInfo
// ① userInfoから特定のURLSchemeを埋め込んだkeyを取得して画面遷移など使用
}
}
}
// MARK: - UNUserNotificationCenterDelegate
@available(iOS 13.0, *)
extension SceneDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
let userInfo = response.notification.request.content.userInfo
// ⑤⑥ userInfoから特定のURLSchemeを埋め込んだkeyを取得して画面遷移など使用
completionHandler()
}
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler([.alert, .badge, .sound])
}
}
備考
様々な記事を調べていたところ、
UNUserNotificationCenterDelegate
のdelegate先はSceneDelegateを使っていたとしても、
共通でAppDelegateに準拠させるで良さそうだったのですが、
実際にiOS13以上で動かしてみるとSceneDelegateで準拠していないとプッシュを受け取れませんでした。
URLスキームによる起動
各通知先
起動方法 | 通知先(~iOS12) | 通知先(iOS13~) |
---|---|---|
アプリ未起動時に発火 | func application(_:open:options:) -> Bool |
func scene(_:willConnectTo:options:) connectionOptions.urlContexts.first?.url
|
バックグラウンド時に発火 | func application(_:open:options:) -> Bool |
scene(_:openURLContexts:) |
実装
extension AppDelegate: UIApplicationDelegate, UIResponder {
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
// ①② urlを使用
return true
}
}
@available(iOS 13.0, *)
extension SceneDelegate: UIResponder, UIWindowSceneDelegate {
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// URLSchemeによる起動
if let url = connectionOptions.urlContexts.first?.url {
// ③
}
}
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
guard let url = URLContexts.first?.url else {
return
}
// ④
}
}
Quick Action(3D Touch)
各通知先
起動方法 | 通知先(~iOS12) | 通知先(iOS13~) |
---|---|---|
アプリ未起動時に受信 | ①func application(_:performActionFor:completionHandler:)
|
③func scene(_:willConnectTo:options:) connectionOptions.shortcutItem
|
バックグラウンド時に受信 | ② 同上 |
④func windowScene(_:,shortcutItem:completionHandler:)
|
実装
extension AppDelegate: UIApplicationDelegate, UIResponder {
func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
// ①②
}
}
@available(iOS 13.0, *)
extension SceneDelegate: UIResponder, UIWindowSceneDelegate {
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Quick Actionによる起動
if let shortcutItem = connectionOptions.shortcutItem {
// ③
}
}
func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
// ④
}
}
Spotlight検索
各通知先
起動方法 | 通知先(~iOS12) | 通知先(iOS13~) |
---|---|---|
アプリ未起動時に発火 | ①func application(_:continue:restorationHandler:) -> Bool
|
③func scene(_:willConnectTo:options:) connectionOptions.userActivities.first
|
バックグラウンド時に発火 | ② 同上 |
④func scene(_:continue:)
|
実装
extension AppDelegate: UIApplicationDelegate, UIResponder {
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
// ①②
return true // 受け取ったuserActivityをハンドリングするか否かを返す
}
}
@available(iOS 13.0, *)
extension SceneDelegate: UIResponder, UIWindowSceneDelegate {
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// spotlight検索による起動
if let userActivity = connectionOptions.userActivities.first {
// ③
}
}
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
// ④
}
}
その他SceneDelegateを扱う上での注意点
- SceneDelegateを使う場合、UIWindowはAppDelegateではなくUIWindowSceneを使う
- UIWindow配下のrootViewControllerにアクセスする場合はSceneDelegate経由で取得する必要がある
-
(UIApplication.shared.delegate as? AppDelegate)?.window?.rootViewController = vc
ではとれない
-
- Windowを新たに作成する場合に、UIWindowSceneから作成する必要がある
- UIWindow配下のrootViewControllerにアクセスする場合はSceneDelegate経由で取得する必要がある