SwiftUI 中的 MVVM 模型

MVVM model in SwiftUI

我想根据 MVVM 将视图与视图模型分开。我将如何在 SwiftUI 中创建模型?我读到应该使用 struct 而不是 class.

例如,我有一个公园模型,您可以在其中植树:

// View Model
struct Park {
  var numberOfTrees = 0
  func plantTree() {
    numberOfTrees += 1 // Cannot assign to property: 'self' is immutable
  }
}

// View
struct ParkView: View {
  var park: Park
  var body: some View {
    // …
  }
}

阅读有关 @State 的内容,使结构有些可变,所以我尝试了:

struct Park {
  @State var numberOfTrees = 0 // Enum 'State' cannot be used as an attribute
  func plantTree() {
    numberOfTrees += 1 // Cannot assign to property: 'self' is immutable
  }
}

我确实直接在View中成功使用了@State。不过,这对分离视图模型代码没有帮助。

我可以使用 class:

class Park: ObservableObject {
  var numberOfTrees = 0
  func plantTree() {
    numberOfTrees += 1
  }
}

…但是我会在使用嵌套在另一个视图模型中的视图模型时遇到麻烦,比如 City:

struct City {
  @ObservedObject var centerPark: Park
}

centerPark 中的更改不会发布,因为 Park 现在是引用类型(至少在我的测试或 here 中不是)。另外,我想知道你是如何使用 struct.

解决这个问题的

作为起点:

// Model
struct Park {
    var numberOfTrees = 0
    mutating func plantTree() {  // `mutating`gets rid of your error
        numberOfTrees += 1
    }
}

// View Model
class CityVM: ObservableObject {
    
    @Published var park = Park() // creates a Park and publishes it to the views
    
    // ... other @Published things ...
    
    // Intents:
    func plantTree() {
        park.plantTree()
    }
}


// View
struct ParkView: View {
    
    // create the ViewModel, which creates the model(s)
    // usually you would do this in the App struct and make available to all views by .environmentObject
    @StateObject var city = CityVM()
    
    var body: some View {
        VStack {
            Text("My city has \(city.park.numberOfTrees) trees.")
            
            Button("Plant one more") {
                city.plantTree()
            }
        }
    }
}

mutating func 是解决方法,但我想我会在下面包含一些其他信息:

我们不将 MVVM 与 SwiftUI 一起使用,因为我们不将 类 用于瞬态视图状态,并且我们不在 MVVM/MVC 意义上控制视图。 SwiftUI 自动为我们创建和更新屏幕上的视图,即 UILabels、UITableView 等。 SwiftUI View 结构本质上已经是视图模型,因此如果您要将其重新创建为一个对象,您不仅会不必要地使您的代码更加复杂,而且还会引入 SwiftUI 试图通过使用结构消除的对象引用错误。使用像 @State@Binding 这样的 属性 包装器,SwiftUI 正在做一些神奇的事情来使结构表现得像一个对象,忽略它不是一个好主意。为了使您的 View 结构更易于测试,您可以将相关的变量提取到一个结构中并使用像这样的变异函数:

// View Model
struct ParkConfig {
  var numberOfTrees = 0
  mutating func plantTree() {
    numberOfTrees += 1
  }
}

struct ContentView {
    @State var parkConfig = ParkConfig()

    var body: some View {
        ParkView(config: $parkConfig)
    }
}

// View
struct ParkView: View {
  @Binding var config: ParkConfig
  var body: some View {
      Button("Click Me") {
          config.plantTree()
      }
  }
}

您可以看到 Apple 在 Data Essentials in SwiftUI WWDC 2020 at 4:18 中展示了这种模式,他说“EditorConfig 可以在其属性上保持不变量并独立测试。而且因为 EditorConfig 是一个值类型,任何更改属性 EditorConfig,就像它的进度一样,是对 EditorConfig 本身的更改。"