Warning: file_get_contents(/data/phpspider/zhask/data//catemap/8/swift/20.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
SwiftUI和MVVM-模型和视图模型之间的通信_Swift_Swiftui_Combine - Fatal编程技术网

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