Data — 数据流与状态的管理

1. @State@Binding #

https://developer.apple.com/documentation/swiftui/state

@State 是 SwiftUI 中用于声明本地状态变量的属性包装器。任何使用到 @State 变量的视图,当该变量变化时,都会重新渲染。

import SwiftUI

struct ContentView: View {
    @State private var counter = 0
    
    var body: some View {
        VStack {
            Text("Counter: \(counter)")
            Button(action: {
                counter += 1
            }) {
                Text("Increment Counter")
            }
        }
    }
}

@Binding 用于在不同视图之间传递和共享 @State 数据,使得一个视图可以绑定到另一个视图的状态。

import SwiftUI  

struct ParentView: View {  
    @State private var isPresented: Bool = false  

    var body: some View {  
        VStack {  
            Button(action: {  
                isPresented.toggle()  
            }) {  
                Text("Toggle Child View")  
            }  
            ChildView(isPresented: $isPresented)  
        }  
    }  
}  

struct ChildView: View {  
    @Binding var isPresented: Bool  

    var body: some View {  
        Text(isPresented ? "Presented!" : "Not Presented")  
    }  
}

2. @Published@ObservedObject #

@Published 是一个属性包装器,主要在 Swift 的 Combine 框架中使用,并且与 SwiftUI 的数据驱动架构紧密集成。它用于将类的属性声明为可以被观察的,从而在属性值发生变化时发出通知给订阅者(通常是 SwiftUI 视图),以触发 UI 刷新。@ObservedObject 用于将一个符合 ObservableObject 协议的对象实例引入到视图中。当被观察对象的 @Published 属性发生变更时,系统会触发视图更新。

工作原理 #

  • 属性声明:当一个属性使用 @Published 修饰时,这个属性会被自动封装到一个发布者中,意味着每当该属性的值发生变化时,都会向其所有订阅者发布更新。

  • 与 ObservableObject 搭配:通常,@Published 用于实现 ObservableObject 协议的类中。ObservableObject 是一个可以被视图订阅的协议,表明它的状态可以随时更新。

  • 通知机制:当 @Published 修饰的属性的值改变时,它会自动通知任何观察它变化的视图刷新视图以反映新的数据。

在 SwiftUI 中,@PublishedObservableObject 经常一起使用,以实现响应式的用户界面。@Published 属性包装器用于标记将会被观察并可能改变的数据属性,而 ObservableObject 协议则用于在对象中的数据变化时通知视图进行刷新和更新。

下面是一些使用 @PublishedObservableObject 的例子:

例子 1: 简单的计数器 #

import SwiftUI

// 定义一个模型使用 ObservableObject
class CounterModel: ObservableObject {
    @Published var count: Int = 0
}

struct ContentView: View {
    // 使用 @ObservedObject 来观察 CounterModel 的变化
    @ObservedObject var counter = CounterModel()
    
    var body: some View {
        VStack {
            Text("Count: \(counter.count)")
                .padding()
            
            Button("Increase") {
                counter.count += 1
            }
        }
    }
}

在这个例子中,每当 count 变量增加时,ContentView 会自动刷新以反映新的值。

例子 2: 切换开关 #

import SwiftUI

class ToggleModel: ObservableObject {
    @Published var isOn: Bool = false
}

struct ToggleView: View {
    @ObservedObject var toggleModel = ToggleModel()
    
    var body: some View {
        VStack {
            Toggle(isOn: $toggleModel.isOn) {
                Text("Toggle is \(toggleModel.isOn ? "ON" : "OFF")")
            }
            .padding()
            
            Button("Toggle manually") {
                toggleModel.isOn.toggle()
            }
        }
    }
}

在这个例子中,通过 Toggle 控件或按钮来改变 isOn 状态,ToggleView 会自动更新显示的文本。

例子 3: 复杂的表单数据 #

import SwiftUI

class UserModel: ObservableObject {
    @Published var name: String = ""
    @Published var age: Int = 0
}

struct UserFormView: View {
    @ObservedObject var user = UserModel()
    
    var body: some View {
        Form {
            Section(header: Text("User Information")) {
                TextField("Name", text: $user.name)
                Stepper(value: $user.age, in: 0...100) {
                    Text("Age: \(user.age)")
                }
            }
        }
        .navigationBarTitle("User Form")
    }
}

在这个例子中,UserFormView 通过表单提供了用户信息的输入,同时响应并更新来自 UserModel 的数据变化。

通过这些例子可以看到,@PublishedObservableObject 结合起来使用,可以非常轻松地实现数据驱动的 UI,为用户提供即时反馈。

3. @StateObject@EnvironmentObject #

@ObservedObject@StateObject@EnvironmentObject 是 SwiftUI 中用于处理和传递数据的属性包装器,它们之间的主要区别在于它们的作用范围和生命周期管理。

  1. @ObservedObject
  • 作用@ObservedObject 用于在视图内部观察和响应来自其他对象的状态变化。
  • 用法:通常用于在视图间共享数据,当被观察对象的属性发生变化时,相应的视图会自动更新。
  1. @StateObject
  • 作用@StateObject 用于在视图内部创建和管理持久化对象或模型。
  • 用法:通常用于在视图内部创建对象,并确保对象在视图销毁和重新加载时保持持久性,不会丢失。
  1. @EnvironmentObject
  • 作用@EnvironmentObject 用于在整个视图层次结构中共享数据模型,并将数据模型注入到子孙视图中。
  • 用法:通常用于全局共享的数据模型,可在任何子孙视图中直接访问和更新该数据。

区别总结:

  • @ObservedObject 用于在视图内部观察和响应来自其他对象的状态变化。
  • @StateObject 用于在视图内部创建和管理持久化对象或模型。
  • @EnvironmentObject 用于在整个视图层次结构中共享数据模型。

示例代码:

import SwiftUI
import Combine

class UserData: ObservableObject {
    @Published var name: String = "John"
}

struct ContentView: View {
    @ObservedObject var observedUser = UserData()
    @StateObject var stateUser = UserData()
    @EnvironmentObject var environmentUser: UserData

    var body: some View {
        VStack {
            Text("Observed: \(observedUser.name)")
            Text("State: \(stateUser.name)")
            Text("Environment: \(environmentUser.name)")
        }
    }
}

在上面的示例中,ContentView 中使用了 @ObservedObject@StateObject@EnvironmentObject 分别观察、创建和共享 UserData 对象。每个属性包装器都有不同的作用和用法,用于处理不同的数据传递和管理需求。

4. @Bindable@Observable #

从 iOS 17 开始,第 2、3 小结涉及的内容已不推荐使用,最好从 @Observable 开始着手。

Migrating from the Observable Object protocol to the Observable macro

Starting with iOS 17, iPadOS 17, macOS 14, tvOS 17, and watchOS 10, SwiftUI provides support for Observation, a Swift-specific implementation of the observer design pattern. Adopting Observation provides your app with the following benefits:

  • Tracking optionals and collections of objects, which isn’t possible when using ObservableObject.

  • Using existing data flow primitives like State and Environment instead of object-based equivalents such as StateObject and EnvironmentObject.

  • Updating views based on changes to the observable properties that a view’s body reads instead of any property changes that occur to an observable object, which can help improve your app’s performance.

  • To take advantage of these benefits in your app, you’ll discover how to replace existing source code that relies on ObservableObject with code that leverages the Observable() macro.

https://developer.apple.com/documentation/swiftui/bindable

@Observable  
class Book: Identifiable {  
    var title = "Sample Book Title"  
    var isAvailable = true  
}  

struct BookEditView: View {  
    @Bindable var book: Book  
    @Environment(\.dismiss) private var dismiss  

    var body: some View {  
        Form {  
            TextField("Title", text: $book.title)  
            
            Toggle("Book is available", isOn: $book.isAvailable)  
            
            Button("Close") {  
                dismiss()  
            }  
        }  
    }  
}

小结 #

@State@Binding 是用于在单个视图或视图之间传递数据和管理状态的属性包装器,而 @Published@ObservedObject 则更多用于跨视图层次结构共享数据和实现数据的响应性更新。以下是它们之间的主要区别和适用场景:

1. @State@Binding #

  • @State:用于在单个视图内部管理可变状态。当状态发生变化时,视图会自动重新渲染。
  • @Binding:用于在不同视图间传递数据,并创建对其他视图状态的引用。可以将 @State 数据绑定到其他视图的 @Binding 属性上。

2. @Published@ObservedObject #

  • @Published:用于在 ObservableObject 中标记需要被观察的属性,当被标记的属性发生变化时发送更新通知。
  • @ObservedObject:用于在视图间观察遵循 ObservableObject 协议的对象的状态。当被观察对象的 @Published 属性发生变化时,相应视图会自动更新。

场景和用途 #

  • 如果只需要在单个视图内部管理状态或者在不同视图之间直接传递数据,可以使用 @State@Binding
  • 当需要在整个应用程序或多个视图之间共享数据,让数据具有响应性更新,并且希望利用 SwiftUI 的自动更新功能时,应使用 @Published@ObservedObject

适用性举例 #

  • 当您想要在两个子视图之间共享数据并同步更改时使用 @State@Binding
  • 当您需要在整个应用程序中共享一个数据模型或实现数据响应性更新时使用 @Published@ObservedObject

综上所述,@State@Binding 主要用于局部状态管理和视图间直接数据传递,而 @Published@ObservedObject 更适用于整个应用程序范围内共享数据和实现数据响应性更新。根据具体的需求和场景,选择合适的属性包装器来实现数据管理和传递是很重要的。

相关文档 #

区别详解 #

在 SwiftUI 中,@EnvironmentObject@StateObject@ObservedObject 是用于管理可观察对象(ObservableObject)的三种关键属性包装器。它们的核心区别在于 对象的所有权生命周期数据传递方式。以下是详细对比及使用场景:


1. @StateObject #

核心特性 #

  • 所有权:由当前视图创建并持有对象。
  • 生命周期:与视图的生命周期绑定,视图销毁时对象才会释放。
  • 初始化时机:仅在视图首次创建时初始化一次,后续视图更新不会重新创建。

使用场景 #

  • 视图内部状态管理:当视图需要独立创建并管理一个 ObservableObject 实例时。
  • 避免重复初始化:确保对象在视图更新时不会被意外重建(如网络请求、计时器等需持久化的操作)。

示例代码 #

class Counter: ObservableObject {
    @Published var count = 0
}

struct ParentView: View {
    // 父视图创建并持有 Counter 实例
    @StateObject var counter = Counter()
    
    var body: some View {
        ChildView(counter: counter)
    }
}

struct ChildView: View {
    @ObservedObject var counter: Counter
    
    var body: some View {
        Text("Count: \(counter.count)")
    }
}

2. @ObservedObject #

核心特性 #

  • 所有权:对象由外部(父视图或其他来源)传入,当前视图不持有对象。
  • 生命周期:由外部管理,当前视图销毁不影响对象。
  • 重新绑定:若外部传入新的对象实例,视图会立即观察新对象。

使用场景 #

  • 接收外部数据源:当对象由父视图、导航传递或初始化参数传入时。
  • 动态切换数据源:需要根据外部条件切换观察不同对象的场景。

示例代码 #

struct ParentView: View {
    @StateObject var counter = Counter()
    
    var body: some View {
        // 将 counter 传递给子视图
        ChildView(counter: counter)
    }
}

struct ChildView: View {
    @ObservedObject var counter: Counter // 从父视图接收
    
    var body: some View {
        Button("Increment") { counter.count += 1 }
    }
}

3. @EnvironmentObject #

核心特性 #

  • 所有权:对象通过环境(Environment)隐式传递,由外部(如根视图)统一管理。
  • 生命周期:由注入环境的位置管理,通常为应用生命周期。
  • 全局共享:无需显式传递,任何子视图均可直接从环境中访问。

使用场景 #

  • 跨视图共享状态:如用户登录信息、主题设置、全局配置等需多层级访问的数据。
  • 避免层层传递:简化深层嵌套视图间的数据传递。

示例代码 #

// 在根视图中注入环境对象
@main
struct MyApp: App {
    @StateObject var user = User()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(user) // 注入到环境
        }
    }
}

struct ContentView: View {
    var body: some View {
        ProfileView()
    }
}

struct ProfileView: View {
    @EnvironmentObject var user: User // 直接从环境获取
    
    var body: some View {
        Text("User: \(user.name)")
    }
}

对比总结 #

特性@StateObject@ObservedObject@EnvironmentObject
所有权当前视图持有外部传入,视图不持有环境全局管理
生命周期与视图绑定由外部管理由注入环境的位置管理
传递方式内部创建显式传递(参数、绑定等)隐式传递(环境)
典型场景视图私有状态父子视图数据传递跨视图共享全局状态
重新初始化视图更新时不重建外部传入新对象时更新环境对象变化时更新

关联概念补充 #

1. ObservableObject #

  • 作用:协议,标记一个类为可观察对象,通过 @Published 属性驱动视图更新。
  • 关键方法objectWillChange 发布变化通知。

2. @Published #

  • 作用:修饰属性,当其值变化时自动触发 ObservableObject 的更新通知。

3. @State 与 @Binding #

  • @State:管理视图私有简单状态(值类型),如 BoolInt 等。
  • @Binding:与父视图共享状态,允许子视图修改父视图的 @State@Binding

4. @Environment #

  • 作用:访问系统环境值(如 colorSchemelocale),与 @EnvironmentObject 不同,不涉及 ObservableObject

常见错误与解决方案 #

  1. 误用 @ObservedObject 导致对象重建

    • 现象:在视图内部使用 @ObservedObject var obj = MyObject(),每次视图更新时重新初始化。
    • 解决:改用 @StateObject 确保对象只创建一次。
  2. 未注入 @EnvironmentObject 导致崩溃

    • 现象:使用 @EnvironmentObject 但未在父视图调用 .environmentObject()
    • 解决:确保在视图层级上游正确注入对象。
  3. 混淆 @State@StateObject

    • 原则@State 用于值类型,@StateObject 用于引用类型(ObservableObject)。

最佳实践 #

  1. 视图内部状态:优先使用 @State@StateObject
  2. 父子数据传递:简单状态用 @Binding,复杂对象用 @ObservedObject
  3. 全局共享状态:统一用 @EnvironmentObject,避免深层传递。
  4. 对象生命周期:确保 @StateObject 不被意外释放,@ObservedObject 及时更新。

通过合理选择这三种属性包装器,可以高效管理 SwiftUI 应用中的数据流与状态更新。

本文共 4030 字,创建于 Oct 27, 2024
相关标签: Swift, 系统设计, ByAI, Xcode, SwiftUI