Tana Gone
Tana Gone
1 min read

Categories

macOS14(2024)以降Observableマクロが使える。2026時点で導入から3年目を迎える。移行ガイドはXcode Documentationをsample code: Observableとすればヒットするのだが、Xcodeを起動せずとも移行の要点を把握できる様に、移行ガイド記載のコードの断片をここに記載する。
Migrating from the Observable Object protocol to the Observable macro: Apple Developer Documentation

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

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

note: Xcode Documentationでは2つ目のLibraryにはObservableObjectプロトコルへの準拠の記載が無いが誤記だと思われる。

SwiftUIに監視させるにはその型(class)にマクロを書く

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

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

Appを構成するWindow全体で値を共有したい場合、@StateObjectプロパティラッパーを使用し、Windowへの注入にはenvironmentObjectモディファイアを使用

// BEFORE
@main
struct BookReaderApp: App {
    @StateObject private var library = Library()
    var body: some Scene {
        WindowGroup {
            LibraryView()
                .environmentObject(library)
        }
    }
}

あれこれ異なる

// AFTER
@main
struct BookReaderApp: App {
    @State private var library = Library()
    var body: some Scene {
        WindowGroup {
            LibraryView()
                .environment(library)
        }
    }
}

Windowに注入された変数の受け取り

// 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)
        }
    }
}

監視されたオブジェクトをWindowで利用。book変数は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)
        }
    }
}

@ObservedObjectプロパティラッパーは不要

// 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)
        }
    }
}

上記コードのBookEditView生成時に引数で与えられたbook変数にはプロパティラッパーが必要

// BEFORE
struct BookEditView: View {
    @ObservedObject var book: Book
    @Environment(\.dismiss) private var dismiss
    
    var body: some View {
        VStack() {
            TextField("Title", text: $book.title)
                .textFieldStyle(.roundedBorder)
                .onSubmit {
                    dismiss()
                }
                
            Button("Close") {
                dismiss()
            }
            .buttonStyle(.borderedProminent)
        }
        .padding()
    }
}

子Windowであれば@Bindableプロパティラッパーを使う

// AFTER
struct BookEditView: View {
    @Bindable var book: Book
    @Environment(\.dismiss) private var dismiss
    
    var body: some View {
        VStack() {
            TextField("Title", text: $book.title)
                .textFieldStyle(.roundedBorder)
                .onSubmit {
                    dismiss()
                }
                
            Button("Close") {
                dismiss()
            }
            .buttonStyle(.borderedProminent)
        }
        .padding()
    }
}