能登 要

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

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

SwiftUI 2はSwiftUIの理念をアプリ全体にまで拡張させたので開発者もそれに追随しよう、という話

SwiftUI2 を始めるにあたっての理屈を構築するための読み物。これまでのiOSアプリ開発者を対象。SwiftUI からiOSアプリをはじめる人にもSwiftUI の学習コストがこれまでに比べて低いと思える内容かもしれない。

1) SwiftUI2

SwiftUI 2は、SwiftUI 1からのアップデートとしてSwiftUI だけでアプリを構築できるようになった、と言われている。

ではどのようにSwiftUI だけでアプリ構築できるようになったか?端的に言うとSwiftUI の概念をアプリ構築側まで拡張させている。

別にSwiftUI の文法とか作法に従ってアプリのフレームワークを構築する必要はないが、これまで以上の開発者を引き寄せるには従来の開発者が慣れ親しんだフレームワークをまずは追い出そう、というのがSwiftUI2 だったりする。

2)クラスフレームワーク

SwiftUI が登場する以前のアプリケーションフレームワークを少し考えてみる。macOSがOSⅩ(10.0)の頃にアプリケーションフレームワークは、クラスフレームワーク郡で構成されたCocoa frameworkでそれ以前の構造体とAPIで構築された開発ツールよりも2000年代の促していると思われた。

Cocoa frameworkは柔軟性が高いアプリケーションフレームワークでプロセッサの切り替え(PowerPCアーキテクチャからIntelアーキテクチャ)、iPhone用アプリケーションフレームワークCocoa touch frameworkへの移植(IntelアーキテクチャからARMアーキテクチャ) を実現している。

クラスフレームワークはオブジェクトの考え方を通じてアプリを構築する。オブジェクトの概念を知っていればコードの意図を読み取る手助けとなるし、アプリを構築する際コード記述は手慣れたものになる。

3)オブジェクトとSwiftUIの混在

SwiftUI の概念はSwiftUI 1(2019)の決定した形を継続し、Swift 2(2020)では改良に留まっている。

SwiftUI の概念は

  1. 構造体(Struct) をベース
  2. 型の厳密さ

が求められる。SwiftUI の概念の元ではオブジェクトの定義は実体化される回数とタイミングは選択できない、オブエクトを参照できる箇所は制限されている、画面UIが全てオブジェクトで気軽にオブジェクトの挙動を差し替えたり(派生クラス)、イベントを登録できる以前できた実装方法(Delegate,Action)は一旦は排除されている。

SwiftUI 1では、SwiftUIと旧来のアプリケーションフレームワークが混在している状態なので、アプリケーションフレームワーク側にオブジェクトの考え方を押し込ん打上で、SwiftUI で構築したコード上から呼び出すことで対処できた。

気をつけて欲しいのはSwiftUI で構築したUIであっても最終的な生成物はオブジェクトとなる点で、オブジェクトとなった時点でオブジェクト-オブジェクト間の処理を呼び出すことができる。

4) アプリから追い出されるオブジェクト

SwiftUI 2となり、アプリ構築層もSwiftUIの概念で記述することができるように強制している。AppとSceneを定義することで簡潔にアプリ構築層を実現できるようになった。

SwiftUI 概念の拡張

簡潔に記述できることは良いとして、SwiftUI の概念がアプリ構築まで適用されたため、このままで はオブジェクトをAppに配置することはできない。

Cocoa framework And SwiftUI 2

5) @StateObjectの登場

ここまできてSwiftUI 2で導入してされた新機能@StateObjectを説明できる。

@StateObjectとは、SwiftUI 1からある@State のオブジェクト版でオブジェクトをSwiftUI の要素に組み込むことができる。

SwiftUI 1の頃からオブジェクトをSwiftUI 内で参照する方法として@ObservedObjectが用意されていたが@ObservedObjectはオブジェクトのライフサイクルを管理しないためコード記述者がオブジェクトのライフサイクルを監視する必要があった。

@StateObjectはSwiftUI を構成するViewやApp の変数として記述可能でライフサイクルもSwiftUI 側が管理してくれるので従来のオブジェクトの取り扱いに近い形になる。

オブジェクトと言ってもなんでも利用できるわけではなく、ObservableObjectの派生クラスのみを指定可能となる。

例えば、SwiftUI でアプリ構築層を作成する場合、App 定義内でObservableObjectの派生クラスを指定することでアプリ構築時と同時に生成されるオブジェクトを作成することができる。

import SwiftUI

@main
struct FooApp: App {
    @StateObject var model = FooModel()
    var body: some Scene {
        WindowGroup {
            ContentView()
              .environmentObject(model)
        }
    }
}

6) @UIApplicationDelegateAdapter

SwiftUI 2でSwiftUI の概念をアプリ構築まで押し広げて完結。というとそうではなく、2020年の段階でも各種プラットフォーム(iOS,macOS,watchOS)ごとのアプリケーションフレームワークに依存する箇所はまだ存在する。

iOS SDKだとUIApplicationDelegate の中に重要な処理が残っている。知っている範疇で列挙すると、

  • プッシュ通知の登録成功/失敗
  • バックグラウンド通知の受け取り
  • Hand-offの振り分け

がある。

プッシュ通知のような受け口が決まっているもの、Hand-offのようにドキュメントのオープンに関わるものはSDKだとUIApplicationDelegateを必要としている。

UIApplicationDelegateはプロトコル(protocol)として定義されている。UIApplicationDelegateプロトコルに準拠したクラスを用意。クラスのインスタンスをUIApplication.shared.delegateプロパティに設定することでアプリ層のイベントを取得できる。

SwiftUI 以前、UIApplicationDelegateプロトコルに準拠したクラスのインスタンスのライフサイクルがアプリのライフサイクルと同じなのでUIApplicationDelegateプロトコルに準拠したクラスでアプリに必要なオブジェクト群を生成していた過去がある。

SwiftUI 以後はUIApplicationDelegateはプロトコルのイベントを受け取るのは1つに限定できないので、Adapter という形でSwiftUI 上のオブジェクトとして定義できるようになっている。

import UIKit

class AppDelegate: UIResponder, UIApplicationDelegate {
    
}

import SwiftUI

@main
struct FooApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

派生クラス定義を渡す特殊な記述なので注意が必要。

@UIApplicationDelegateAdapter で定義したクラスにアプリケーションのロジック層を記述すれば良いという向きもあるが、UIApplicationDelegateはiOS SDKの定義なので(将来マルチプラットフォームに対応するかは分からないが)SwiftUI を提供するApple としては分けて考えることを奨励しているように見て取れる。

7) 依存性注入(Dependency Injection)

SwiftUIと依存性注入の考え方は対立しない。ただしApplicationDelegate にDIコンテナを定義するような実装をしている場合は独自クラスにDIコンテナを移した方が良いかもしれない。

充電報告さんの実装時に、iOSアプリの他、TodayExtensionとAppleWatchにSwiftUI ベースのUIを組み込む際にApplicationDelegateのクラス定義にあったルートのDIコンテナを独自定義クラスに移動しソースコード共有することでコードの共通化を図ることができた。

以下は、Swinject を使ったルートのDIコンテナの例。

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    static let container = Container() { container in
    
    }
}
class RootContainer {
    static let container = Container() { container in
        #if os(iOS)
            // iOS 依存コード
        #elseif os(watchOS)
            // watchOS 依存コード
        #endif
    }
}

RootContainerをiOS、watchOS両方のターゲットに加えつつ、コンテナ定義でターゲットOS毎に調整を加えている。