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

相关推荐
今天也想MK代码2 天前
在Swift开发中简化应用程序发布与权限管理的解决方案——SparkleEasy
前端·javascript·chrome·macos·electron·swiftui
東三城7 天前
【ios】---SwiftUI开发从入门到放弃
ios·swiftui·swift·1024程序员节
今天也想MK代码10 天前
基于swiftui 实现3D loading 动画效果
ios·swiftui·swift
胖虎111 天前
SwiftUI(五)- ForEach循环创建视图&尺寸类&安全区域
ios·swiftui·swift·foreach·安全区域
zyosasa17 天前
SwiftUI 精通之路 11: 栅格布局
前端·swiftui·swift
小溪彼岸21 天前
【iOS小组件实战】灵动岛实时进度通知
swiftui·swift
提笔忘字的帝国25 天前
【ios】SwiftUI 混用 UIKit 的 Bug 解决:UITableView 无法滚动到底部
swiftui·bug·xcode
zyosasa25 天前
SwiftUI 精通之路 09:ForEach 视图构造器的基础应用
swiftui·swift
提笔忘字的帝国25 天前
【ios】在 SwiftUI 中实现可随时调用的加载框
ios·swiftui·xcode·swift
小溪彼岸25 天前
【iOS小组件】小组件App ID、Group ID、描述文件
swiftui·swift