こんにちは。スタディサプリ採用担当の鈴木です。今回の記事は、@nkmrhによる「サクッとわかる SwiftUI in WWDC 2020」です!是非、ご覧ください!
※こちらの記事はは2020年7月23日に投稿されたものです。
こんにちは。4月に入社したiOSエンジニアの中村(@nkmrh)です。 東京もそろそろ梅雨が明けて夏がやってきますね。 さて、先月は WWDC 2020 がオンラインで開催されました。SwiftUI の新機能も発表され、いよいよ実戦投入の気運が高まってきているのではないでしょうか!
以下の2つのセッションで SwiftUI の新機能が紹介されていましたので、本稿ではこれらのセッションで、特にポイントとなりそうな項目をピックアップしてご紹介したいと思います。
Data Essentials in SwiftUI
このセッションでは、ビューとデータのバインドの方法について説明されていました。 新しく追加された機能の中に以下のものがありました。
- Property Wrappers
- @StateObject
- @SceneStorage
- @AppStorage
- Modifier
- onChange
- 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 をキャッチアップしていく上で参考になれば幸いです。