SwiftUI和MVVM-模型和视图模型之间的通信
我一直在试验SwiftUI和MVVM-模型和视图模型之间的通信,swift,swiftui,combine,Swift,Swiftui,Combine,我一直在试验SwiftUI中使用的MVVM模型,但有些东西我还不太了解 SwiftUI使用@observeObject/@ObservedObject检测视图模型中的更改,这些更改触发对主体属性的重新计算以更新视图 在MVVM模型中,这是视图和视图模型之间的通信。我不太明白的是模型和视图模型是如何通信的 当模型更改时,视图模型如何知道这一点?我考虑手动使用新的Combine框架在视图模型可以订阅的模型内创建发布者 然而,我创建了一个简单的示例,我认为这种方法非常乏味。有一个名为Game的模型,它
SwiftUI
中使用的MVVM模型,但有些东西我还不太了解
SwiftUI
使用@observeObject
/@ObservedObject
检测视图模型中的更改,这些更改触发对主体
属性的重新计算以更新视图
在MVVM模型中,这是视图和视图模型之间的通信。我不太明白的是模型和视图模型是如何通信的
当模型更改时,视图模型如何知道这一点?我考虑手动使用新的Combine
框架在视图模型可以订阅的模型内创建发布者
然而,我创建了一个简单的示例,我认为这种方法非常乏味。有一个名为Game
的模型,它包含Game.Character
对象数组。字符具有可以更改的强度
属性
那么,如果视图模型更改了角色的强度
属性,该怎么办?为了检测这种变化,模型必须订阅游戏中的每一个角色(可能还有许多其他角色)。这不是有点太多了吗?或者有很多出版商和订阅者是正常的吗
还是我的示例没有正确地遵循MVVM?我的视图模型是否应该将实际模型游戏作为属性?如果是这样,有什么更好的方法
//我的模型
班级游戏{
类字符{
let name:String
变量强度:Int
init(名称:String,强度:Int){
self.name=名称
自我力量=力量
}
}
变量字符:[字符]
初始化(字符:[字符]){
self.characters=字符
}
}
// ...
//我的视图模型
类ViewModel:ObservableObject{
let objectWillChange=PassthroughSubject()
让游戏:游戏
初始化(游戏:游戏){
self.game=游戏
}
公共函数changeCharacter(){
self.game.characters[0]。力量+=20
}
}
//现在我创建了一个模型游戏的演示实例。
让鲍勃=游戏。角色(名字:“鲍勃”,力量:10)
让爱丽丝=游戏。角色(名字:“爱丽丝”,力量:42)
让游戏=游戏(角色:[鲍勃,爱丽丝])
// ..
//然后,对于我的一个视图,我初始化其视图模型如下:
MyView(视图模型:视图模型(游戏:游戏))
//当我现在更改角色时,例如通过调用ViewModel的方法“changeCharacter()”,我如何触发视图(以及显示该角色的所有其他活动视图)重新绘制?
我希望我的意思很清楚。这很难解释,因为它令人困惑
谢谢 要提醒您的视图中观察到的@变量
,请将对象更改为
PassthroughSubject()
还有,打电话
objectWillChange.send()
在您的changeCharacter()
函数中。我花了最后几个小时来处理代码,我想我已经想出了一个很好的方法。我不知道这是否是预期的方式,或者它是否是正确的MVVM,但它似乎可以工作,而且实际上非常方便
我将在下面发布一个完整的工作示例,供任何人试用。它应该是现成的
以下是一些想法(可能完全是垃圾,我对那些东西一无所知。如果我错了,请纠正我:)
- 我认为
查看模型
可能不应该包含或保存模型中的任何实际数据。这样做将有效地创建已保存在模型层中的内容的副本。数据存储在多个地方会导致各种同步和更新问题,在更改任何东西时都必须考虑这些问题。我尝试过的所有东西最终都变成了一大块难看的代码
- 为模型内的数据结构使用类并不能很好地工作,因为它会使检测更改变得更加麻烦(更改属性不会更改对象)。因此,我改为使用
字符
类结构
- 我花了几个小时试图弄清楚如何在
模型层
和视图模型
之间传递更改。我尝试设置自定义发布者、跟踪任何更改并相应更新视图模型的自定义订阅者,我考虑让模型
订阅视图模型
,以及建立双向通信,等等。没有任何效果。这感觉很不自然但有一点:模型不必与视图模型通信。事实上,我认为根本不应该。这可能就是MVVM的意义所在。MVVM教程中显示的可视化也显示了这一点:
(来源:)
- 这是单向连接。视图模型从模型中读取数据,并可能对数据进行更改,但仅此而已
因此,我没有让
模型
告诉视图模型
任何更改,而是让视图
通过使模型成为可观察对象
来检测对模型
的更改。每次更改时,都会重新计算视图,从而调用视图模型上的方法和属性。然而,视图模型
只是从模型中获取当前数据(因为它只访问而从不保存数据),并将其提供给视图视图模型不必知道模型是否已更新。没关系
- 考虑到这一点,让这个例子起作用并不难
下面是一个示例应用程序来演示一切。它只显示所有字符的列表,同时显示显示单个字符的第二个视图
进行更改时,两个视图将同步
// Character.swift
import Foundation
class Character: Decodable, Identifiable{
let id: Int
let name: String
var strength: Int
init(id: Int, name: String, strength: Int) {
self.id = id
self.name = name
self.strength = strength
}
}
// GameModel.swift
import Foundation
struct GameModel {
var characters: [Character]
init() {
// Now let's add some characters to the game model
// Note we could change the GameModel to add/create characters dymanically,
// but we want to focus on the communication between view and viewmodel by updating the strength.
let bob = Character(id: 1000, name: "Bob", strength: 10)
let alice = Character(id: 1001, name: "Alice", strength: 42)
let leonie = Character(id: 1002, name: "Leonie", strength: 58)
let jeff = Character(id: 1003, name: "Jeff", strength: 95)
self.characters = [bob, alice, leonie, jeff]
}
func increaseCharacterStrength(id: Int) {
let character = characters.first(where: { $0.id == id })!
character.strength += 10
}
func selectedCharacter(id: Int) -> Character {
return characters.first(where: { $0.id == id })!
}
}
// GameViewModel
import Foundation
class GameViewModel: ObservableObject {
@Published var gameModel: GameModel
@Published var selectedCharacterId: Int
init() {
self.gameModel = GameModel()
self.selectedCharacterId = 1000
}
func increaseCharacterStrength() {
self.gameModel.increaseCharacterStrength(id: self.selectedCharacterId)
}
func selectedCharacter() -> Character {
return self.gameModel.selectedCharacter(id: self.selectedCharacterId)
}
}
// GameView.swift
import SwiftUI
struct GameView: View {
@ObservedObject var gameViewModel: GameViewModel
var body: some View {
NavigationView {
VStack {
Text("Tap on a character to increase its number")
.padding(.horizontal, nil)
.font(.caption)
.lineLimit(2)
CharacterList(gameViewModel: self.gameViewModel)
CharacterDetail(gameViewModel: self.gameViewModel)
.frame(height: 300)
}
.navigationBarTitle("Testing MVVM")
}
}
}
struct GameView_Previews: PreviewProvider {
static var previews: some View {
GameView(gameViewModel: GameViewModel())
.previewDevice(PreviewDevice(rawValue: "iPhone XS"))
}
}
//CharacterDetail.swift
import SwiftUI
struct CharacterDetail: View {
@ObservedObject var gameViewModel: GameViewModel
var body: some View {
ZStack(alignment: .center) {
RoundedRectangle(cornerRadius: 25, style: .continuous)
.padding()
.foregroundColor(Color(UIColor.secondarySystemBackground))
VStack {
Text(self.gameViewModel.selectedCharacter().name)
.font(.headline)
Button(action: {
self.gameViewModel.increaseCharacterStrength()
self.gameViewModel.objectWillChange.send()
}) {
ZStack(alignment: .center) {
Circle()
.frame(width: 80, height: 80)
.foregroundColor(Color(UIColor.tertiarySystemBackground))
Text("\(self.gameViewModel.selectedCharacter().strength)").font(.largeTitle).bold()
}.padding()
}
Text("Tap on circle\nto increase number")
.font(.caption)
.lineLimit(2)
.multilineTextAlignment(.center)
}
}
}
}
struct CharacterDetail_Previews: PreviewProvider {
static var previews: some View {
CharacterDetail(gameViewModel: GameViewModel())
}
}
// CharacterList.swift
import SwiftUI
struct CharacterList: View {
@ObservedObject var gameViewModel: GameViewModel
var body: some View {
List {
ForEach(gameViewModel.gameModel.characters) { character in
Button(action: {
self.gameViewModel.selectedCharacterId = character.id
}) {
HStack {
ZStack(alignment: .center) {
Circle()
.frame(width: 60, height: 40)
.foregroundColor(Color(UIColor.secondarySystemBackground))
Text("\(character.strength)")
}
VStack(alignment: .leading) {
Text("Character").font(.caption)
Text(character.name).bold()
}
Spacer()
}
}
.foregroundColor(Color.primary)
}
}
}
}
struct CharacterList_Previews: PreviewProvider {
static var previews: some View {
CharacterList(gameViewModel: GameViewModel())
}
}
// SceneDelegate.swift (only scene func is provided)
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
let gameViewModel = GameViewModel()
window.rootViewController = UIHostingController(rootView: GameView(gameViewModel: gameViewModel))
self.window = window
window.makeKeyAndVisible()
}
}
struct GameModel {
// build your model
}
struct Game: View {
@State var m = GameModel()
var body: some View {
// access m
}
// actions
func changeCharacter() { // mutate m }
}