能登 要

札幌在住のiOSアプリ開発者。SwiftUI により分割されたデバイス間を縦横にやりとりできる考え方に転換しています。

iOSアプリ開発者。2009年のiPhoneアプリ開発開始時期から活動。開発言語のアップデートの中でSwiftUIおよび周辺技術に着目中。

既存iOSプロジェクトをSwiftUI Appへ移行する - アプリ開発者はSwiftUIにおけるAppDelegate/SceneDelegateの扱いをよく理解していない?

プロジェクトのLife CycleをUIKit App DelegateからSwiftUI Appへ移行するシンプルな作業に隠された。アプリの複雑さはどこに押し込められたのかという話。

note:

内容はSwiftUI への移行を迫ることを意図したものではなく移行時の注意点について記述した読み物。SwiftUIを取り入れるかについてはFucking SwiftUI - Cheat SheetのFAQに同意する。

端的にいうと:

iOSアプリのプロジェクトはAppDelegate/SceneDelegateから当面逃げられない。iOSアプリの煩雑さはAppDelegate/SceneDelegateに残っている。SceneDelegate機能そのものは維持されていることは意識の片隅に残した方が良い。

1) 背景

WWDC2020で公開されたiOS14 SDKはSwiftUI 2nd major releaseとしてAppleが謳う重要なリリース。Multiplatformと置き換わったエクステンションはSwiftUIで記述可能となる。

iOSアプリに旧来と区別するためLife Cycleの考え方が増えUIKit ベースだけだったものからSwiftUIも選択できるようになった。

Xcode12にも手が加えられ新規プロジェクトタイミングでLife CycleをUIKit App DelegateとSwiftUI Appのいずれかが選択可能に(macOSもLife Cycle項目としてAppKit App DelegateとSwiftUI Appを選択可能)。

Xcode12からはMultiplatformアプリを新規作成できる。プロジェクトを新規作成するとmacOS/iOSの双方で動作するSwiftUI Appのソースコードが生成される。 ←macOS/iOSで動作するアプリを作るのであれば魅力的な選択肢。

役目を終えたのかXcode11以前のCross-Platformプロジェクトは選択できなくなった。

2) 動機

既存プロジェクトでもLife CycleをSwiftUI Appに移行したい。

3) SwiftUI Appへ移行する様々な理由

Multiplatform対応などSwiftUIを通して提供される機能が魅力的に映るかで捉え方は異なる。

iOS13 SDK及びからSwiftUIベースで作成したiOSアプリのLife CycleをUIKit App DelegateからSwiftUI Appに移行できるのであればMultiplatform対応が見えてくる。

iOS13 SDKより前に始めたiOSアプリプロジェクトで且つ、iOS13 SDKで追加されたAppDelegate/SceneDelegateの煩雑に感じるのであれば、SwiftUI 2nd major releaseで導入された簡潔な記述に魅力を感じるかもしれない。

Multiplatform対応を志し、ロジック層は維持しつつ既存UIを一度放棄して始める場合も、Life CycleをSwiftUI Appに切り替える同期になるはず。

これまでのUIKit App Delegateの表現力は魅力的だが、Apple の開発者が言い切っているバグを生むリスクを向き合うのは避けられない。

声に出して読みたいWWDC2020① | Irimasu Densan Planning - いります電算企画

3-1) 想定移行プロジェクト

UIKit App DelegateからSwiftUI Appへい移行する際に以下を想定する。iOSアプリプロジェクトを対象とするMultiplatformは今回は対象としない。

  • iOS13 SDK(Xcode11) で作成した(or Xcode iOS - Life CycleにUIKit App Delegate選択)新規プロジェクト
  • iOS - Single View App
  • Language - Swift
  • User Interface - Storyboard or SwiftUI

UI周りの使い回しは可能だが本題から外れるので代替画面としてSwiftUI のTextを渡す。

Text("この画面は一時的なものです")
    .padding()

4) 移行はシンプル

iOS13 SDK、iOS12 SDK以前に作成したプロジェクトのLife CycleをAppKit App DelegateからSwiftUI App へ移行する事はMultiplatform対応やUIの移植に重点を置かないのであれば比較的簡単に実施できる。

やる事は、

  • 既存のAppDelegateに付加されている@宣言(@UIApplicationMain or @main)を除外
  • SwiftUIのApp を追加
  • Appの変数として@UIApplicationDelegateAdaptor宣言でAppDelegateの変数を作成する

となる。

AppDelegateを含んだSwiftUI のAppのコードを示す。

import SwiftUI

@main
struct FooApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        WindowGroup {
            Text("この画面は一時的なものです")
                .padding()
        }
    }
}

@UIApplicationDelegateAdaptorについては以前にBlogでまとめている。 @UIApplicationDelegateAdapterについて読み物 - SwiftUIからアプリ始めた人むけ | Irimasu Densan Planning - いります電算企画

@UIApplicationDelegateAdaptorを使用することで、既存のAppDelegateを利用することができる。Life CycleでSwiftUI App を選んだとしてもSwiftUI 側でAppDelegateのインスタンスを適切なタイミングで生成しイベントを呼び出してくれる。

class AppDelegate: UIResponder, UIApplicationDelegate {
    var shortcutItemToProcess: UIApplicationShortcutItem?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        return true
    }
}

AppDelegateはプッシュ通知登録などのiOSの重要なイベントが残っており、AppDelegateを残しておく必要がある。iOS14 SDKで新規iOSアプリでLife CycleでSwiftUI Appを選択した場合はAppDelegateをプロジェクトに作成する必要に迫られるだろう。

note:

起動時のイベントであるUIApplicationDelegate.application(didFinishLaunchingWithOptions launchOptions:) も呼ばれる。ただしLife CycleでSwiftUI Appを選択した場合はWindow - Root ViewController を生成する処理は無視される。

5) iOSのShortcut機能はどこに?

iOS9から導入されたショートカット機能はiOS14 SDK以降も機能する。ショートカットはinfo.plistに登録するかアプリケーション起動後にUIApplication.shared.shortcutItems に登録する。もしくは両方を組み合わせて使用できる。iOS14だとホーム画面を長押しすると表示されるメニューにアプリ機能のショートカットとして動作する。

iOS Shortcut

利用者がショートカット選択後、イベントを受け取る事でアプリからショートカットイベントを捕捉できる。ショートカットイベントはiOS9 SDK以降であればAppDelegate に定義されている。

public protocol UIApplicationDelegate : NSObjectProtocol {
    @available(iOS 9.0, *)
    optional func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void)
}

ところがこのイベントは呼び出されない。SwiftUIの用意した仕組み(.onChange, .onOpenURL)を使ってショートカットイベントを補足できるのか?といえばそうでもない。Life CycleにSwiftUI App を選ぶ場合はiOSのShortcut機能は使えないのだろうか?

6) 不要にならないSceneDelegate

iOSのShortcut機能のショートカットイベントはUIWindowSceneDelegate定義で補足できる。

public protocol UIWindowSceneDelegate : UISceneDelegate {
    optional func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void)
}

6-1) Sceneの導入方法

iOS13 SDKにてAppDelegate/SceneDelegate機構が導入されたタイミングで、既存プロジェクトへのSceneDelegate対応をスキップした開発者に向けてSceneDelegateの導入方法をXcodeのiOSアプリテンプレートベースで説明すると、info.plist にUIApplicationSceneManifestキー以下に定義される。 SceneConfiguration

UIApplicationSceneManifestは複数のSceneを許可するか、UISceneConfigurations で複数のSceneを定義できる。

Sceneの指定は、インスタンスを生成するクラス名とプログラム上で呼び出すための文字列で構成されている。

コード上として必要なのは、

  • 新規にSceneDelegateを追加
  • AppDelegateに呼び出し先Scene指定イベントを実装する

ショートカットイベントを補足するSceneDelegateのコードを示す。

import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

    }

    func sceneDidDisconnect(_ scene: UIScene) {

    }

    func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {

    }
}

note:

SceneDelegateを導入するとwillConnectTo sessionイベントでWindowsのインスタンスを生成し、画面リソースとして使用されるがLife CycleにSwiftUI Appを導入した場合はWindowsのインスタンスを生成は無視される。

2020年9月12日訂正: SceneDelegateを導入するとwillConnectTo sessionイベントでWindowsのインスタンスを生成し、画面リソースとして使用される。Life CycleをSwiftUI Appにしても画面リソースとして使用されるので(AppDelegateのイベントとは異なり)Windowsインスタンスの生成処理をコメントアウトが必要。

AppDelegateでのScene生成イベントコードを示す。

import UIKit

class AppDelegate: UIResponder, UIApplicationDelegate {

    // MARK: UISceneSession Lifecycle

    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        // Called when a new scene session is being created.
        // Use this method to select a configuration to create the new scene with.
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }

    func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {

    }
}

configurationForConnecting connectingSceneSessionの戻り値UISceneConfigurationのインスタンスで渡した情報を基にSceneが生成される。

SwiftUIAppDelegateSceneDelegate

Sceneの指定を行わない場合は内部的にSwiftUI.AppSceneDelegateが使用されている。SwiftUI.AppSceneDelegateは内部的な定義なのでコード上で参照することはできずデバッグ中に UISceneConfigurationのdelegateClassとして確認できる。

8) まとめ

  • SwiftUI 2nd major releaseよりSwiftUI でApp/Sceneが導入された
  • SwiftUI のイベント機構に押し込めないデバイス固有の機能はUIAppDeleegate/UISceneDelegate に残されている
  • Sceneを導入していないプロジェクトではAppDelegate/SceneDelegateを手動で導入する必要があるので注意が必要
  • すでにSceneを導入しているプロジェクトにLife CycleをSwiftUI に以降する場合もScene生成コード(は有効なので)を削除する必要はない

参考