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用于多视图数据同享。

一个基本的雏形代码如下:

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 {
    ...
  }
}

咱们来改造一个例子,来详细领会一下。

SwiftUI中如何使用 ObservableObject、StateObject

一个简单的shopping list, 代码也很简单。如下所示:

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来处理这个问题。详细代码如下:

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的数据情况。

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的数据重复增加。

SwiftUI中如何使用 ObservableObject、StateObject

其实咱们的数据只需要在FruitViewModel初始化时加载咱们的数据就能够了。所以咱们会把fruitViewModel.getFruits 移除到FruitViewModelinit办法中去

在FruitViewModel中增加一下代码即可:

init() {
  getFruits()
}

它会在FruitViewModel初始化时由系统调用

以上就是悉数示例,可是有一个问题,咱们的fruitViewModel特点现在是由 @ObservedObject润饰的,在这里咱们其实是希望fruitViewModel的生命周期是SwiftUI办理的,究竟如果SwiftUI消失了,这个数据也就不需要了。在结合最开端两个特点的比照成果。这里咱们把ObservedObject改成StateObject。

悉数代码在此:

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