在SwiftUI中有很多属性包装器,本节会介绍两个常用的包装器@ObservableObject, @StateObject, 前面我们介绍过@State 和 @Binding。它们都是非常重要的,也是必须掌握的
@ObservableObject和@StateObject的区别和用法:
@ObservableObject用于将一个可观察对象(ObservableObject)注入到一个视图中。常用于在视图间共享数据模型。
@StateObject用于在一个视图内部管理一个可观察对象的状态。
主要区别是:
- @ObservableObject 属性包装器用于标记类(class)的属性 ,表示该属性是一个可观察对象,也就是说,当该属性发生变化时,SwiftUI 会自动更新依赖于该属性的视图,@StateObject 属性包装器与 @ObservableObject 类似,都用于跟踪对象的变化,但是有一个重要的区别。@StateObject 用于在 SwiftUI 视图层次结构中创建一个全局唯一的对象,这个对象可以在子视图中共享和访问,而不需要在每个子视图中传递该对象
- @ObservableObject生命周期由父视图管理 ,@StateObject生命周期由当前视图管理。
- @ObservableObject适合在多个视图间共享数据 ,@StateObject适合在单个视图内使用。
- @StateObject会在视图消失时自动移除生命周期由SwiftUI管理 ,@ObservableObject需手动移除。
所以:@StateObject用于单视图状态管理,@ObservableObject用于多视图数据共享。
一个基本的雏形代码如下:
SwiftUI
class Model: ObservableObject { // 需要遵守 ObservableObject 协议
@Published var data = [] // 使用 StateObjectsh 属性包装器修饰的对象,最少内部需要保证有一个被 @publisher 修饰的对象
}
struct ContentView: View {
@StateObject var model = Model() // 该视图管理
var body: some View {
OtherView(model: model) // 传递给子视图使用
}
}
struct OtherView: View {
@ObservableObject var model: Model // 由父视图注入
var body: some View {
...
}
}
我们来改造一个例子,来具体体会一下。
一个简单的shopping list, 代码也很简单。如下所示:
SwiftUI
struct FruitModel: Identifiable, Hashable {
let id: String = UUID().uuidString
let name: String
let count: Int
}
struct ViewModelSample: View {
@State private var fruits: [FruitModel] = []
@State private var isLoading: Bool = true
var body: some View {
NavigationView {
List {
if isLoading {
ProgressView()
}
ForEach(fruits, id: .self) { fruit in
HStack {
Text("(fruit.count)")
.font(.subheadline)
Text(fruit.name)
.font(.subheadline)
.fontWeight(.semibold)
}
.frame(height: 45)
}
.onDelete(perform: delete)
}
.listStyle(.grouped)
.navigationTitle("Shopping list")
.onAppear(perform:getFruits)
}
}
func delete(indexSet: IndexSet) {
fruits.remove(atOffsets: indexSet)
}
func getFruits() {
isLoading = true
DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: {
let appleFruit = FruitModel(name: "Apples", count: 3)
let orangeFruit = FruitModel(name: "Orange", count: 1)
let watermelonFruit = FruitModel(name: "Watermelon", count: 1)
let bananaFruit = FruitModel(name: "Banana", count: 19)
fruits.append(appleFruit)
fruits.append(orangeFruit)
fruits.append(watermelonFruit)
fruits.append(bananaFruit)
isLoading = false
})
}
}
struct ViewModelSample_Previews: PreviewProvider {
static var previews: some View {
ViewModelSample()
}
}
以上代码初看没啥问题,但是有点经验的开发人员就会看出UI和逻辑部分的代码完全在一个类里面,没有任何区分。当然在逻辑少的情况下也是没问题的,但是往往一个真实的项目,代码量远远不止这些。所以我们接下来会把以上的代码分开,把UI和逻辑数据处理部分完全分开。
使用增加一个VM来解决这个问题。具体代码如下:
SwiftUI
class FruitViewModel: ObservableObject {
@Published var fruits: [FruitModel] = []
@Published var isLoading: Bool = true
func delete(indexSet: IndexSet) {
fruits.remove(atOffsets: indexSet)
}
func getFruits() {
isLoading = true
DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: {
let appleFruit = FruitModel(name: "Apples", count: 3)
let orangeFruit = FruitModel(name: "Orange", count: 1)
let watermelonFruit = FruitModel(name: "Watermelon", count: 1)
let bananaFruit = FruitModel(name: "Banana", count: 19)
self.fruits.append(appleFruit)
self.fruits.append(orangeFruit)
self.fruits.append(watermelonFruit)
self.fruits.append(bananaFruit)
self.isLoading = false
})
}
}
struct ViewModelSample: View {
@ObservedObject var fruitViewModel: FruitViewModel = FruitViewModel()
......
}
参考最开始的代码雏形,我们对代码做了以下改动。
- 让 FruitViewModel 遵循 ObservableObject 协议
- 需要被外部观察的对象使用 @Published 修饰
- 把整个数据处理的逻辑放在了 FruitViewModel 内部。
- 在 ViewModelSample 内部,在使用外部数据时,使用@ObservedObject 来修饰
以上改造对功能完全没有改造,只是对代码进行了重构。
现在有另一个需求, 我们需要在另一个页面分析统计一下shoppinglist的数据情况。
SwiftUI
struct AnalyzeView: View {
@ObservedObject var fruitViewModel: FruitViewModel
var body: some View {
ZStack {
Color.white.edgesIgnoringSafeArea(.all)
VStack(spacing: 20) {
Text("(fruitViewModel.fruits.count)")
.font(.largeTitle)
.fontWeight(.semibold)
.underline()
.foregroundColor(.white)
Text("Total count")
.foregroundColor(.white)
.font(.headline)
.fontWeight(.semibold)
}
.padding(40)
.background(Color.blue.cornerRadius(10))
}
}
}
页面接收一个FruitViewModel对象,简单的显示了一下shoppinglist的数量值
但是上述代码依然有问题,因为我们的数据加载方法放在了父层级的onAppear方法中,因为onAppear方法会不止一次加载,所以会导致list的数据重复添加。
其实我们的数据只需要在FruitViewModel初始化时加载我们的数据就可以了。所以我们会把fruitViewModel.getFruits 移除到FruitViewModel 的init方法中去
在FruitViewModel中添加一下代码即可:
SwiftUI
init() {
getFruits()
}
它会在FruitViewModel初始化时由系统调用
以上就是全部示例,但是有一个问题,我们的fruitViewModel属性目前是由 @ObservedObject 修饰的,在这里我们其实是希望fruitViewModel的生命周期是SwiftUI管理的,毕竟如果SwiftUI消失了,这个数据也就不需要了。在结合最开始两个属性的对比结果。这里我们把ObservedObject改成StateObject。
全部代码在此:
SwiftUI
import SwiftUI
struct FruitModel: Identifiable, Hashable {
let id: String = UUID().uuidString
let name: String
let count: Int
}
class FruitViewModel: ObservableObject {
@Published var fruits: [FruitModel] = []
@Published var isLoading: Bool = true
init() {
getFruits()
}
func delete(indexSet: IndexSet) {
fruits.remove(atOffsets: indexSet)
}
func getFruits() {
isLoading = true
DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: {
let appleFruit = FruitModel(name: "Apples", count: 3)
let orangeFruit = FruitModel(name: "Orange", count: 1)
let watermelonFruit = FruitModel(name: "Watermelon", count: 1)
let bananaFruit = FruitModel(name: "Banana", count: 19)
self.fruits.append(appleFruit)
self.fruits.append(orangeFruit)
self.fruits.append(watermelonFruit)
self.fruits.append(bananaFruit)
self.isLoading = false
})
}
}
struct ViewModelSample: View {
@StateObject var fruitViewModel: FruitViewModel = FruitViewModel()
var body: some View {
NavigationView {
List {
if fruitViewModel.isLoading {
ProgressView()
}
ForEach(fruitViewModel.fruits, id: .self) { fruit in
HStack {
Text("(fruit.count)")
.font(.subheadline)
Text(fruit.name)
.font(.subheadline)
.fontWeight(.semibold)
}
.frame(height: 45)
}
.onDelete(perform: fruitViewModel.delete(indexSet:))
}
.listStyle(.grouped)
.navigationTitle("Shopping list")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing, content: {
NavigationLink(
destination: AnalyzeView(fruitViewModel: fruitViewModel)
) {
Image(systemName: "cellularbars")
}
})
}
}
}
}
struct AnalyzeView: View {
@ObservedObject var fruitViewModel: FruitViewModel
var body: some View {
ZStack {
Color.white.edgesIgnoringSafeArea(.all)
VStack(spacing: 20) {
Text("(fruitViewModel.fruits.count)")
.font(.largeTitle)
.fontWeight(.semibold)
.underline()
.foregroundColor(.white)
Text("Total count")
.foregroundColor(.white)
.font(.headline)
.fontWeight(.semibold)
}
.padding(40)
.background(Color.blue.cornerRadius(10))
}
}
}
struct ViewModelSample_Previews: PreviewProvider {
static var previews: some View {
ViewModelSample()
}
}
针对常用属性包装器,会单独讲讲具体区别和使用场景。
大家有什么看法呢?欢迎留言讨论。
公众号:RobotPBQ