SwiftUI中如何使用 ObservableObject、StateObject

在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()
    ......
}

参考最开始的代码雏形,我们对代码做了以下改动。

  1. 让 FruitViewModel 遵循 ObservableObject 协议
  2. 需要被外部观察的对象使用 @Published 修饰
  3. 把整个数据处理的逻辑放在了 FruitViewModel 内部。
  4. 在 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 移除到FruitViewModelinit方法中去

在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

相关推荐
麦兜*1 天前
Swift + Xcode 开发环境搭建终极指南
开发语言·ios·swiftui·xcode·swift·苹果vision pro·swift5.6.3
大熊猫侯佩3 天前
「内力探查术」:用 Instruments 勘破 SwiftUI 卡顿迷局
swiftui·debug·xcode
HarderCoder3 天前
深入理解 SwiftUI 的 ViewBuilder:从隐式语法到自定义容器
swiftui·swift
东坡肘子3 天前
我差点失去了巴顿(我的狗狗) | 肘子的 Swift 周报 #098
swiftui·swift·apple
黄鹤的小姨子5 天前
SwiftUI 劝退实录:AI 都无能为力,你敢用吗?
swiftui
麦兜*5 天前
【swift】SwiftUI动画卡顿全解:GeometryReader滥用检测与Canvas绘制替代方案
服务器·ios·swiftui·android studio·objective-c·ai编程·swift
东坡肘子10 天前
苹果首次在中国永久关闭了一家 Apple Store | 肘子的 Swift 周报 #097
swiftui·swift·apple
大熊猫侯佩14 天前
WWDC 25 玻璃态星际联盟:SwiftUI 视图协同“防御协议”
swiftui·swift·wwdc
东坡肘子17 天前
Xcode 26 beta 4,要崩我们一起崩 | 肘子的 Swift 周报 #096
swiftui·swift·apple
吴Wu涛涛涛涛涛Tao18 天前
SwiftUI 打造 TikTok 风格的滑动短视频播放器
ios·swiftui