8
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

WWDC 2023: SwiftUIパフォーマンスを劇的に改善するObservationフレームワークの全貌

Last updated at Posted at 2024-06-28

はじめに

WWDC 2023で、AppleはSwift標準ライブラリの新しいメンバーであるObservationフレームワークを紹介しました。このフレームワークは、長年にわたって開発者が直面してきたSwiftUIのビューの無効な更新問題を緩和することを目的としています。

Observationフレームワークの必要性

Swift 5.9以前は、Appleは開発者に対して参照型のプロパティ変更を効率的に観察するための統一された仕組みを提供していませんでした。KVOはNSObjectのサブクラスに限定され、Combineはプロパティレベルの精密な観察を提供できず、いずれもクロスプラットフォームのサポートが不十分でした。

さらにObservableObjectプロトコルに基づいて実装されています。これにより、不必要なビューの更新が多発し、SwiftUIアプリのパフォーマンスに影響を与えていました。

これらの制限を改善するために、Swift 5.9でObservationフレームワークが導入されました。

ObservableObjectの観察

ObservableObjectを使用してシンプルなUIを実装し、ボタンをクリックするとUIの更新ログが出力されるようにしてみます。


import SwiftUI

struct ContentView: View {

    var eventService = EventService()

    var body: some View {
        Root()
    }
}
class Store: ObservableObject {

    @Published var a = 1
    @Published var b = "hello"

}

struct Root: View {

    @StateObject var store = Store()

    var body: some View {
        print("Root be reloaded")
        return
            VStack {
                AView(store: store)
                BView(store: store)
                Button {
                    store.a = store.a + 1
                } label: {
                    Text("+")
                }
                Button {
                    store.b = store.b + "1"
                } label: {
                    Text("change text")
                }
            }
    }
}

struct AView: View {

    @ObservedObject var store: Store

    var body: some View {
        print("AView be reloaded")
        return view
    }

    var view: some View {
        Text("\(store.a)")
    }
}

struct BView: View {

    @ObservedObject var store: Store

    var body: some View {
        print("BView be reloaded")
        return view
    }

    var view: some View {
        Text(store.b)
    }
}


スクリーンショット 2024-06-27 14.29.04.png

「+」と「change text」というボタンを両方クリックした後に出たのロッグは下記です。

Root be reloaded
AView be reloaded
BView be reloaded
Root be reloaded
AView be reloaded
BView be reloaded
Root be reloaded
AView be reloaded
BView be reloaded

Observationで改善したら

ObservableObjectを@Observableに置き換えて、同じUI操作を実現する方法を見てみましょう。効果を確認します。

import SwiftUI
import Observation

struct ContentView: View {

    @State var store = Store()

    var body: some View {
        return Root()
    }
}

@Observable
class Store {

    var a = 1
    var b = "hello"

}

struct Root: View {

    @State var store = Store()

    var body: some View {
        print("Root be reloaded")
        return
            VStack {
                AView(store: store)
                BView(store: store)
                Button {
                    store.a = store.a + 1
                } label: {
                    Text("+")
                }

                Button {
                    store.b = store.b + "1"
                } label: {
                    Text("change text")
                }
            }
    }
}

struct AView: View {

    var store: Store

    var body: some View {
        print("AView be reloaded")
        return Text("\(store.a)")
    }

}

struct BView: View {

    var store: Store

    var body: some View {
        print("BView be reloaded")
        return Text(store.b)
    }

}

スクリーンショット 2024-06-27 14.31.49.png

「+」と「change text」というボタンを両方クリックした後に出たのロッグは下記です。

Root be reloaded
AView be reloaded
BView be reloaded
AView be reloaded
BView be reloaded

Observationフレームワークを使用したメリット

宣言の簡素化:

@Observableを使用することで、StoreクラスはObservableObjectプロトコルを採用する必要がなくなります。また、プロパティを@Publishedでマークする必要もありません。

コードの冗長性の削減:

@Observableを使うことで、クラスの宣言がシンプルになり、プロパティに特別な注釈を追加する必要がなくなります。これにより、コードがより簡潔で読みやすくなります。

ビューのリロードの最適化:

Observationフレームワークでは、ビューのプロパティごとの変更をトラッキングするため、不必要なリロードを避けることができます。例えば、AViewのstore.aが変更されたときだけリロードされ、store.bの変更はAViewのリロードを引き起こしません。

計算プロパティのサポート

@Observableでは計算プロパティも観察可能です。例えば、fullNameのような計算プロパティを追加しても、変更を追跡できます。

import SwiftUI
import Observation

@Observable
class User {

    var firstName: String = "塩中"
    var lastName: String = "優子"

    var fullName: String {
        firstName + " " + lastName
    }

    init(firstName: String, lastName: String) {
        self.firstName = firstName
        self.lastName = lastName
    }
}

struct ContentView: View {

    @State var user = User(firstName: "塩中", lastName: "優子")

    var body: some View {
        Text(user.fullName)
        Button {
            user.firstName = "青池"
            user.lastName = "紘一"
        } label: {
            Text("名前を変える")
        }
    }
}

環境へのシンプルな注入

Observationフレームワーク導入以前は、SwiftUIではObservableObjectプロトコルを採用したオブジェクトを環境に注入するために、@EnvironmentObjectを使用していました。このプロトコルと属性を使って、ビュー間で共有するデータを管理していました。

以下に、Observationフレームワークを導入する前にどのようにして環境にオブジェクトを注入し、ビューで使用していたかの例を示します。


import SwiftUI

@main
struct eventnewsApp: App {

    @StateObject var user = User()

    // ObservableObjectを環境に注入
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(user)
        }
    }
}

class User: ObservableObject {

    @Published var firstName: String = "塩中"
    @Published var lastName: String = "優子"

}

struct ContentView: View {

    // 環境からObservableObjectを注入
    @EnvironmentObject var user: User

    var body: some View {
        Text(user.firstName + user.lastName)
        Button {
            user.firstName = "青池"
            user.lastName = "紘一"
        } label: {
            Text("名前を変える")
        }
    }
}

または、EnvironmentKeyを使って環境に注入することもできる


import SwiftUI

struct UserKey: EnvironmentKey {

    static var defaultValue = User()

}

extension EnvironmentValues {

    var user: User {
        get { self[UserKey.self] }
        set { self[UserKey.self] = newValue }
    }

}

@main
struct eventnewsApp: App {

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }

}

@Observable
class User {

    var firstName: String = "塩中"
    var lastName: String = "優子"

}

struct ContentView: View {

    // 環境からObservableObjectを注入
    @Environment(\.user) var user

    var body: some View {
        Text(user.firstName + user.lastName)
        Button {
            user.firstName = "青池"
            user.lastName = "紘一"
        } label: {
            Text("名前を変える")
        }
    }

}


カスタムEnvironmentKeyとオプショナル値の注入の両方の方法は、忘れて注入しなかった場合に発生するプレビューのクラッシュ問題を完全に解決します。特に、EnvironmentKeyは、開発者にデフォルト値を提供する能力を持たせます。

まとめ

Observationフレームワークを使用することで、宣言が簡素化され、コードがより直感的で簡潔になります。また、ビューのリロードが最適化され、パフォーマンスが向上します。これにより、開発者はコードの可読性と保守性を向上させつつ、アプリケーションのパフォーマンスを改善することができます。

8
4
1

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
8
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?