こんにちは。4月に入社したiOSエンジニアの中村(@nkmrh)です。 東京もそろそろ梅雨が明けて夏がやってきますね。 さて、先月は WWDC 2020 がオンラインで開催されました。SwiftUI の新機能も発表され、いよいよ実戦投入の気運が高まってきているのではないでしょうか!
以下の2つのセッションで SwiftUI の新機能が紹介されていましたので、本稿ではこれらのセッションで、特にポイントとなりそうな項目をピックアップしてご紹介したいと思います。
Data Essentials in SwiftUI
このセッションでは、ビューとデータのバインドの方法について説明されていました。 新しく追加された機能の中に以下のものがありました。
- Property Wrappers
@StateObject
@SceneStorage
@AppStorage
- Modifier
onChange
順番に見ていきましょう。
@StateObject
class Store: ObservableObject { @Published var count = 0 } struct ParentView: View { @StateObject private var store = Store() var body: some View { ChildView() } }
上記のコードは @StateObject
の使用例です。はじめに ParentView
の body
メソッドが呼ばれる前に store
プロパティがインスタンス化されます。(ParentView
がインスタンス化されるタイミングではなく)。それ以降 ParentView
が再インスタンス化される場合でも、store
プロパティの状態は保持され続けます。
@ObservedObject
を使用して Store
オブジェクトを ParentView
にバインドした場合、ParentView
が再インスタンス化されるタイミングで Store
オブジェクトも初期化されてしまう為、状態を保持しておくには Store
オブジェクトを外から注入させる必要がありました。
また、@ObservedObject
は ParentView
が再インスタンス化される度にインスタンス化され、ヒープメモリを圧迫しパフォーマンスの悪化原因となる為、@StateObject
を使用することが推奨されていました。@StateObject
のライフサイクルは SwiftUI が自動的に管理してくれるようです。
文章での説明だと分かりづらいと思いますので、サンプルコードで @StateObject
と @ObservedObject
を利用した場合での挙動の違いを確認してみて下さい。
@SceneStorage
@SceneStorage("selection") var selection: String?
@SceneStorage
を使うと Scene
単位でデータを永続化できます。 @State
プロパティのように View にバインドして使います。Scene
単位なので、保存されたデータは Scene 間で共有されません。 内部的に UserDefaults
は使われていないそうです。セッション内では Scene-Wide Source of Truth
と紹介されていました。
@AppStorage
@AppStorage("updateArtwork") private var updateArtwork = true
@AppStorage
はこれまでの UserDefaults
と同じです。UserDefaults
を View にバインドできるようになったので便利そうですね。
onChange modifier
struct ContentView: View { @State var count = 0 var body: some View { VStack { Button("Increment count") { count += 1 } Text("count \(count)") .onChange(of: count) { newCount in print(newCount) } } } }
onChange
modifier を使用すると @State
プロパティ等の値の変化を監視することができます。
以上が Data Essentials in SwiftUI セッションの中からキャッチアップしておきたい内容だと思います。
App Essentials in SwiftUI
このセッションでは SwiftUI における新しいアプリのライフサイクルの書き方が紹介されていました。
Xcode12以降から SwiftUI アプリケーションを新規作成すると、新規作成ダイアログの Life Cycle
の項目から SwiftUI App
と UIKit App Delegate
のどちらかを選択できるようになっており、後者は従来の AppDelegate
と SceneDelegate
を使用したボイラープレートで、View を表示する為に UIHostingController
を使いますが、前者は SwiftUI のコードだけで作成されます。
次のコードは UIKit App Delegate
選択時に生成される従来のボイラープレートです。UIHostingController
を使い View を表示しています。
class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { let contentView = ContentView() if let windowScene = scene as? UIWindowScene { let window = UIWindow(windowScene: windowScene) window.rootViewController = UIHostingController(rootView: contentView) self.window = window window.makeKeyAndVisible() } }
そして、次のコードは SwiftUI App
選択時に作成されるボイラープレートです。たったの8行で驚きました。セッション内では、"It's 100% functional app" と紹介されていたのが印象的でした。
@main struct MyApp: App { var body: some Scene { WindowGroup { ContentView() } } }
上記のコードを見ていきましょう。
@main
まずはじめに目に付くのが @main
です。これは、
@main 属性で Swift5.3
で導入されたものです。この属性を struct
, class
, enumration
に適用すると、プログラムのエントリポイントを含むことを示します。適用するには main
関数を提供する必要があり、SwiftUI では App
プロトコルが main 関数を提供しています。
App プロトコル
次に、App プロトコルです。App プロトコルはアプリの構造や動作を表すタイプです。App プロトコルに準拠するには、1つ以上の Scene
を返す body
プロパティを実装する必要があります。
WindowGroup
WindowGroup
は macOS
と iPadOS
のマルチウィンドウに対応し、WindowGroup
以下の View 階層がマルチウィンドウ起動時のテンプレートとなります。iOS
watchOS
tvOS
の場合は、1つのフルスクリーンウィンドウとなります。これにより、プラットフォームが違っても1つのコードで対応できるようになると解説されていました。
App Scene View の関係
App, Scene, View の関係は以下のようになり、WindowGroup
が Scene を管理してくれます。
従来の Delagete プロトコルへの対応方法
App
プロトコルで従来の UISceneDelegate
や UIApplicationDelegate
に対応するにはどうしたら良いのでしょうか。この点についてはセッション内では解説されていませんでしたが、以下のようにすることで対応できるようです。
UISceneDelegate
に対応するには、ScenePhase 列挙子を Environment
から取得し、onChange()
メソッドの引数に設定することで、ScenePhase
の値を監視することで対応できるようです。onChange()
メソッドは上述の Data Essentials in SwiftUI で紹介されていましたね。
@main struct MyApp: App { @Environment(\.scenePhase) private var scenePhase var body: some Scene { WindowGroup { ContentView() } .onChange(of: scenePhase) { newScenePhase in switch newScenePhase { case .active, .inactive, .background: print(newScenePhase) default: fatalError() } } } }
※ Xcode beta2 の時点では、まだ beta の為なのか background しか呼ばれていないようでした
UIApplicationDelegate
に対応するには UIApplicationDelegateAdaptor PropertyWrappers を使います。初期化時に UIApplicationDelegate
に準拠した型を渡すと、デリゲートメソッドが呼ばれるようになります。
@main struct MyApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate ... } final class AppDelegate: NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { print(#function) return true } func applicationWillTerminate(_ application: UIApplication) { print(#function) } }
macOS の場合は NSApplicationDelegateAdaptor が用意されています
App Essentials in SwiftUI セッションでは上記の内容が解説されていました。
まとめ
本稿では、上記2セッションの内容を解説しました。SwiftUI が使われていくにつれて、マルチウィンドウ対応や macOS 対応等も大事なポイントとなってくるのかもしれません。SwiftUI をキャッチアップしていく上で参考になれば幸いです。
こちらの記事を読まれて Quipper に興味をお持ちいただいた皆様、ぜひ Quipper に遊びに来ませんか? 以下 Wantedly ページよりお気軽にご連絡ください! https://www.wantedly.com/companies/quipper/projects