LoginSignup
14
7

More than 1 year has passed since last update.

SceneDelegateで様々なアプリの起動経路に対応する

Posted at

はじめに

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)
バックグラウンド時に受信
同上

同上

実装

AppDelegate.swift
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])
    }
}

SceneDelegate.swift
@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:)

実装

AppDelegate.swift
extension AppDelegate: UIApplicationDelegate, UIResponder {
    func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
        // ①② urlを使用
        return true
    }
}

SceneDelegate.swift
@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:)

実装

AppDelegate.swift
extension AppDelegate: UIApplicationDelegate, UIResponder {
    func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
        // ①②
    }
}

SceneDelegate.swift
@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:)

実装

AppDelegate.swift
extension AppDelegate: UIApplicationDelegate, UIResponder {
    func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
        // ①②
        return true // 受け取ったuserActivityをハンドリングするか否かを返す
    }
}

SceneDelegate.swift
@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から作成する必要がある

参考

14
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
7