iOS 17:告别ObservableObject,迎接@Observable

发布时间 2023-10-22 15:31:10作者: 逆行

自iOS 17(iPadOS 17, macOS 14)之后,SwiftUI 提供了一种新的宏:@Observable
可以把它当做是 ObservableObject 的优化版。在系统版本允许的情况下,我们应该优先使用@Observable
@ObservableObservableObject更好的地方在于:

  1. 写法上更加简洁;
  2. 性能更优化。@Observable能精确到对象属性变化来更新view,也就是说,只有view读取的属性变化,才会触发view的刷新。而ObservableObject的更新是无差别的,无论view读取了ObservableObject对象的哪些属性,只要ObservableObject被视为变化,与之关联的view都会刷新。

对比修饰符

ObservableObject 搭配的修饰符有:

  1. @StateObject;
  2. @ObservedObject;
  3. @EnvironmentObject;

@Observable 搭配的修饰符有:

  1. @State;
  2. @Bindalbe;
  3. @Environment;

新的写法

代码引用自 官方示例

界面层次:

  • BookReaderApp
    • LibraryView
      • BookView
        • BookEditView

首先定义 @Observable对象,

@Observable class Library {
    var name = "sample library"
    var books: [Book] = [Book(), Book(), Book()] 
    @ObservationIgnored var hello = "hello" // @ObservationIgnored 用于标记属性为不可监听。使得该属性值的变化不触发view更新。
}

@Observable class Book: Identifiable {
    var title = "Sample Book Title"
    let id = UUID() 
}


如果只是想让界面及时刷新数据?

struct BookView: View {
    var book: Book     // 无需修饰符,view自动监听对象的属性变化
    var body: some View {
            Text(book.title)
    }
}

如果还想“绑定”属性,好让控件来修改属性?

struct BookView: View {
    @State var book: Book = Book()  // @State 修饰
    var body: some View {
          TextField("Title", text: $book.title)
    }
}

如果我没办法在当前View创建book对象,它是从外部传进来的呢?

struct BookView: View {
    @Bindable var book: Book  // @ Bindable 修饰,简直就是对象版的@Binding
    var body: some View {
          TextField("Title", text: $book.title)
    }
}

假如有个对象,多个view共用,我不想一个个传递,该怎么办呢?
不妨来一套 @State.environment(...)@ Environment 组合拳

@main
struct BookReaderApp: App {
    @State private var library = Library() // @State 修饰,使其能作为可绑定参数传递

    var body: some Scene {
        WindowGroup {
            LibraryView()
                .environment(library) // 通过注入 .environment() 修改器注入
        }
    }
}

//-----------------

struct LibraryView: View {
    @Environment(Library.self) private var library // 通过 @Environment 修饰符读出
    
    var body: some View {                          
        List(library.books) { book in
            BookView(book: book)       
        }        
    }
}

此时,你想把@Environment变量与控件绑定,发现无法用 $读取(比如$library❌)。
解决方法就是使用@Bindable

You can use the Bindable property wrapper on properties and variables to an Observable object. This includes global variables, properties that exists outside of SwiftUI types, or even local variables.

比如:

struct TitleEditView: View {
    @Environment(Book.self) private var book

    var body: some View {
        @Bindable var book = book
        TextField("Title", text: $book.title)
    }
}

还可以这样用:

struct LibraryView: View {
    @State private var books = [Book(), Book(), Book()]

    var body: some View {
        List(books) { book in
            @Bindable var book = book
            TextField("Title", text: $book.title)
        }
    }
}

参考资料

https://developer.apple.com/forums/thread/732658
https://developer.apple.com/documentation/swiftui/bindable
https://medium.com/@jywvgkchm/transitioning-to-the-new-observable-macro-in-swiftui-8b249673ab1e