🪶

ObservableObjectプロトコルからObservableマクロへの移行

に公開

概要

ObservableObjectを使わなくても簡潔に書く方法があった!
XでObservableObjectを使うよりobservableマクロなるものを使った方がいいの投稿を見て気になりました!

https://x.com/sakiyamaK/status/1992936256066695649?s=20

https://developer.apple.com/documentation/SwiftUI/Migrating-from-the-observable-object-protocol-to-the-observable-macro#Use-the-Observable-macro

What ObservableObject?

ObservableObject
変更前に値を発生するパブリッシャーを持つオブジェクトのタイプ。

「publisher」はここでは「値を発生させるもの」という意味で使われています。SwiftUIのCombineフレームワークにおける概念で、データの変更を購読者に通知する役割を担います。

@StateだとViewの中に状態管理するロジック書いてしまうけど、ObservableObjectを使ったクラスを用意すると分離させることができる!

カウンタを例にしたもの

Combineを書いてあげないと使えない。

import SwiftUI
import Combine

class Counter: ObservableObject {
    var objectWillChange: ObservableObjectPublisher
    
    @Published var count: Int = 0
    
    init(count: Int) {
        self.count = count
        self.objectWillChange = ObservableObjectPublisher()
    }
    
    func increment() {
        count += 1
        objectWillChange.send()
    }
}

struct CounterView: View {
    @StateObject private var counter = Counter(count: 0)
    var body: some View {
        Button("Increment: \(counter.count)") {
            counter.increment()
        }
    }
}

#Preview {
    CounterView()
}

でも実はなくても使えるのですよ

これだけで動く👇
これがマクロの力なのか。。。。

import SwiftUI
//import Combine

//class Counter: ObservableObject {
//    var objectWillChange: ObservableObjectPublisher
//    
//    @Published var count: Int = 0
//    
//    init(count: Int) {
//        self.count = count
//        self.objectWillChange = ObservableObjectPublisher()
//    }
//    
//    func increment() {
//        count += 1
//        objectWillChange.send()
//    }
//}

// 修正後のソースコード
@Observable class Counter {
    var count: Int
    
    init(count: Int) {
        self.count = count
    }
    
    func increment() {
        count += 1
    }
}

struct CounterView: View {
    @State private var counter = Counter(count: 0)
    var body: some View {
        Text("@Observableでも動く!")
        Button("Increment: \(counter.count)") {
            counter.increment()
        }
    }
}

#Preview {
    CounterView()
}

公式のBookを例に解説していく

公式の英語の解説をNaniを使用して翻訳してみる。
https://developer.apple.com/documentation/Combine/ObservableObject

Observable ObjectプロトコルからObservableマクロへの移行

SwiftにおけるObservationの利点を活用するために、既存のアプリケーションをアップデートしましょう。

ダウンロード
iOS 17.0+
iPadOS 17.0+
macOS 14.0+
Xcode 15.0+

概要
iOS 17、iPadOS 17、macOS 14、tvOS 17、watchOS 10以降では、SwiftUIはオブザーバーデザインパターンのSwift固有の実装であるObservationをサポートしています。 Observationを採用することで、アプリケーションに以下の利点が得られます。

ObservableObjectを使用している場合は不可能だった、オプショナル型やオブジェクトのコレクションの追跡。

StateObjectやEnvironmentObjectのようなオブジェクトベースの代替手段の代わりに、StateやEnvironmentのような既存のデータフロープリミティブの使用。

Observableオブジェクトに対するプロパティの変更ではなく、ビューのbodyが読み取るobservableプロパティの変更に基づいてビューを更新することにより、アプリケーションのパフォーマンスを向上させることができます。

これらの利点をアプリケーションで活用するために、ObservableObjectに依存する既存のソースコードを、Observable()マクロを活用するコードに置き換える方法を学びましょう。

このサンプルをダウンロードすると、移行済みのサンプルアプリを確認できます。移行前のバージョンを見るには、「Monitoring data changes in your app」で提供されているサンプルをダウンロードしてください。移行前のバージョンを使って、この記事と一緒にコードを記述することもできます。

Observableマクロを使用する
既存のアプリでObservationを採用するには、まずデータモデルの型にあるObservableObjectをObservable()マクロに置き換えることから始めます。Observable()マクロは、コンパイル時にソースコードを生成し、その型にObservationサポートを追加します。

従来のもの

// BEFORE
import SwiftUI


class Library: ObservableObject {
    // ...
}

新しい書き方

// AFTER
import SwiftUI


@Observable class Library {
    // ...
}

次に、observableプロパティからPublishedプロパティラッパーを削除します。Observationは、プロパティをobservableにするためにプロパティラッパーを必要としません。代わりに、ビューのようなオブザーバーとの関係におけるプロパティのアクセス可能性が、そのプロパティがobservableであるかどうかを決定します。

従来のもの

// BEFORE
@Observable class Library {
    @Published var books: [Book] = [Book(), Book(), Book()]
}

新しい書き方

// AFTER
@Observable class Library {
    var books: [Book] = [Book(), Book(), Book()]
}

オブザーバーからアクセス可能でも追跡したくないプロパティがある場合は、そのプロパティに@ObservationIgnoredマクロを適用します。

段階的に移行する
アプリケーション全体でObservableObjectプロトコルを一括して置き換える必要はありません。代わりに、段階的に変更を加えることができます。まず、1つのデータモデル型をObservable()マクロを使用するように変更することから始めましょう。アプリケーションでは、異なるオブザーベーションシステムを使用するデータモデル型を混在させることができます。ただし、SwiftUIはデータモデル型が使用するオブザーベーションシステム(ObservableとObservableObject)に基づいて、変更を異なる方法で追跡します。

追跡方法に基づいて、アプリの動作にわずかな違いが生じる場合があります。例えば、Observable()として追跡する場合、SwiftUIはobservableプロパティが変更され、ビューのbodyがそのプロパティを直接読み取るときにのみビューを更新します。bodyが読み取らないobservableプロパティが変更されても、ビューは更新されません。対照的に、ObservableObjectとして追跡する場合、ObservableObjectインスタンスの公開されたプロパティのいずれかが変更されると、ビューがその変更されたプロパティを読み取っていなくてもビューが更新されます。


その他のソースコードを移行する
これまでのサンプルアプリへの変更点は、LibraryにObservable()マクロを適用し、ObservableObjectプロトコルのサポートを削除しただけです。アプリは依然としてStateObjectのようなObservableObjectデータフロープリミティブを使用してLibraryのインスタンスを管理しています。もしアプリをビルドして実行すると、SwiftUIは期待通りにビューを更新します。これは、StateObjectやEnvironmentObjectのようなデータフロープロパティラッパーがObservable()マクロを使用する型をサポートしているためです。SwiftUIはこのサポートを提供することで、アプリがソースコードの変更を段階的に行えるようにしています。

しかし、Observationを完全に採用するには、データモデル型を更新した後、StateObjectの使用をStateに置き換えてください。例えば、以下のコードでは、メインアプリ構造がLibraryのインスタンスを作成し、それをStateObjectとして保存しています。また、environmentObject(_:)修飾子を使ってLibraryインスタンスを環境に追加しています。

従来のもの

// BEFORE
@main
struct BookReaderApp: App {
    @StateObject private var library = Library()


    var body: some Scene {
        WindowGroup {
            LibraryView()
                .environmentObject(library)
        }
    }
}

LibraryがObservableObjectに準拠しなくなったため、コードはStateObjectの代わりにStateを使用し、environmentObject(:)修飾子の代わりにenvironment(:)修飾子を使用してライブラリを環境に追加するように変更できます。

新しい書き方

// AFTER
@main
struct BookReaderApp: App {
    @State private var library = Library()


    var body: some Scene {
        WindowGroup {
            LibraryView()
                .environment(library)
        }
    }
}

LibraryがObservationを完全に採用する前に、もう1つの変更が必要です。以前は、LibraryViewはEnvironmentObjectプロパティラッパーを使用して環境からLibraryインスタンスを取得していました。しかし、新しいコードでは、代わりにEnvironmentプロパティラッパーを使用します。

従来のもの

// BEFORE
struct LibraryView: View {
    @EnvironmentObject var library: Library


    var body: some View {
        List(library.books) { book in
            BookView(book: book)
        }
    }
}

新しい書き方

// AFTER
struct LibraryView: View {
    @Environment(Library.self) private var library
    
    var body: some View {
        List(library.books) { book in
            BookView(book: book)
        }
    }
}

ObservedObjectプロパティラッパーを削除する
サンプルアプリの移行を完了させるために、データモデル型BookをObservationをサポートするように変更します。具体的には、型宣言からObservableObjectを削除し、Observable()マクロを適用します。その後、observableプロパティからPublishedプロパティラッパーを削除します。

従来のもの

// BEFORE
class Book: ObservableObject, Identifiable {
    @Published var title = "Sample Book Title"
    
    let id = UUID() // A unique identifier that never changes.
}

新しい書き方

// AFTER
@Observable class Book: Identifiable {
    var title = "Sample Book Title"
    
    let id = UUID() // A unique identifier that never changes.
}

次に、BookView内のbook変数からObservedObjectプロパティラッパーを削除します。Observationを採用する場合、このプロパティラッパーは不要です。これは、SwiftUIがビューのbodyが直接読み取るあらゆるobservableプロパティを自動的に追跡するためです。例えば、book.titleが変更されると、SwiftUIはBookViewを更新します。

従来のもの

// BEFORE
struct BookView: View {
    @ObservedObject var book: Book
    @State private var isEditorPresented = false
    
    var body: some View {
        HStack {
            Text(book.title)
            Spacer()
            Button("Edit") {
                isEditorPresented = true
            }
        }
        .sheet(isPresented: $isEditorPresented) {
            BookEditView(book: book)
        }
    }
}

新しい書き方

// AFTER
struct BookView: View {
    var book: Book
    @State private var isEditorPresented = false
    
    var body: some View {
        HStack {
            Text(book.title)
            Spacer()
            Button("Edit") {
                isEditorPresented = true
            }
        }
        .sheet(isPresented: $isEditorPresented) {
            BookEditView(book: book)
        }
    }
}

動作はこんな感じになります。
https://youtube.com/shorts/vqvwIAjOFSE

最後に

アドベントカレンダー用に記事を書いてみようと思い今回気になっていた話題である「ObservableObjectプロトコルからObservableマクロへの移行」という題材で記事を書いてみました。以前からObservable Object protocolを使っていたので、Observable macroに移行するとコードの記述量も減って見やすいなと思い個人開発してるアプリもこちらに移行するのをやってみたいと思います。

完成品のソースコードはこちら

Discussion