在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 {
...
}
}
咱们来改造一个例子,来详细领会一下。
一个简单的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()
......
}
参阅最开端的代码雏形,咱们对代码做了以下改动。
- 让 FruitViewModel 遵循 ObservableObject 协议
- 需要被外部调查的目标运用 @Published 润饰
- 把整个数据处理的逻辑放在了 FruitViewModel 内部。
- 在 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的数据重复增加。
其实咱们的数据只需要在FruitViewModel初始化时加载咱们的数据就能够了。所以咱们会把fruitViewModel.getFruits 移除到FruitViewModel的init办法中去
在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