我一直在试验SwiftUI
中使用的MVVM模型,但有些事情我还不太了解。
SwiftUI
使用@ObservableObject
/ @ObservedObject
检测视图模型中的更改,这些更改触发重新计算body
属性以更新视图。
在MVVM模型中,这是视图和视图模型之间的通信。我不太了解的是模型和视图模型如何通信。
当模型改变时,视图模型应该如何知道?我考虑过手动使用新的Combine
框架在视图模型可以订阅的模型内部创建发布者。
但是,我认为我创建了一个简单的示例,使此方法非常乏味。有一个名为Game
的模型,其中包含Game.Character
个对象的数组。角色的strength
属性可以更改。
那么,如果视图模型更改了字符的strength
属性,该怎么办?为了检测到这种变化,模型必须订阅游戏具有的每个角色(可能还有许多其他事物)。那不是太多吗?还是拥有许多发布者和订阅者是正常现象?
还是我的示例未正确跟随MVVM?我的视图模型是否应该没有实际的模型game
作为属性?如果是这样,有什么更好的方法?
// My Model
class Game {
class Character {
let name: String
var strength: Int
init(name: String, strength: Int) {
self.name = name
self.strength = strength
}
}
var characters: [Character]
init(characters: [Character]) {
self.characters = characters
}
}
// ...
// My view model
class ViewModel: ObservableObject {
let objectWillChange = PassthroughSubject<ViewModel, Never>()
let game: Game
init(game: Game) {
self.game = game
}
public func changeCharacter() {
self.game.characters[0].strength += 20
}
}
// Now I create a demo instance of the model Game.
let bob = Game.Character(name: "Bob", strength: 10)
let alice = Game.Character(name: "Alice", strength: 42)
let game = Game(characters: [bob, alice])
// ..
// Then for one of my views, I initialize its view model like this:
MyView(viewModel: ViewModel(game: game))
// When I now make changes to a character, e.g. by calling the ViewModel's method "changeCharacter()", how do I trigger the view (and every other active view that displays the character) to redraw?
我希望我的意思很清楚。难以解释,因为它令人困惑
谢谢!
答案 0 :(得分:1)
我花了最后几个小时来处理代码,我想我已经找到了一种非常好的方法。我不知道这是否是预期的方式,或者它是否是正确的MVVM,但它似乎可以正常工作并且很方便。
我将在下面发布一个完整的工作示例,任何人都可以尝试。它应该开箱即用。
这里有一些想法(可能是完全的垃圾,我对这些东西一无所知。如果我错了,请纠正我:))
我认为view models
可能不应包含或保存模型中的任何实际数据。这样做将有效地创建model layer
中已保存内容的副本。将数据存储在多个位置会导致各种同步和更新问题,您在更改任何内容时都必须考虑这些问题。我尝试过的一切最终都是巨大的,难以理解的丑陋代码。
在模型中为数据结构使用类并不能很好地工作,因为这会使检测更改更加麻烦(更改属性不会更改对象)。因此,我将Character
类改为struct
。
我花了数小时试图弄清楚如何在model layer
和view model
之间传达变化。我尝试设置自定义发布者,跟踪任何更改并相应更新视图模型的自定义订阅者,我考虑让model
订阅view model
来建立双向通信,等等。出来。感觉不自然。 但这就是问题:模型不必与视图模型进行通信。实际上,我认为完全不应该。可能就是MVVM。 raywenderlich.com的MVVM教程中显示的可视化效果也显示了这一点:
(来源:https://www.raywenderlich.com/4161005-mvvm-with-combine-tutorial-for-ios)
这是单向连接。视图模型从模型中读取数据,也许可以对数据进行更改,仅此而已。
因此,我并没有让model
告诉view model
任何更改,而是通过使模型成为{{1 }}。每次更改时,都会重新计算视图,该视图调用view
上的方法和属性。但是,model
仅从模型中获取当前数据(因为它仅访问而从未保存它们)并将其提供给视图。 视图模型完全不必知道模型是否已更新。没关系。
考虑到这一点,使示例变得很容易。
这是演示所有内容的示例应用程序。它只显示所有字符的列表,同时显示另一个显示单个字符的视图。
进行更改时,两个视图都是同步的。
ObservableObject
答案 1 :(得分:0)
要提醒@Observed
中的View
变量,请将objectWillChange
更改为
PassthroughSubject<Void, Never>()
也打电话
objectWillChange.send()
在您的changeCharacter()
函数中。
答案 2 :(得分:0)
感谢Quantm在上面发布了示例代码。我遵循了您的示例,但做了一些简化。我所做的更改:
有了这些更改,MVVM设置非常简单,并且视图模型和视图之间的双向通信全部由SwiftUI框架提供,无需添加任何其他调用来触发任何更新,这一切都发生了自动。希望这也有助于回答您的原始问题。
以下是与上面的示例代码相同的工作代码:
// 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()
}
}
答案 3 :(得分:0)
简短的答案是使用@State,每当状态属性更改时,都会重建视图。
长答案是根据SwiftUI更新MVVM范例。
通常,要使某个东西成为“视图模型”,需要将某些绑定机制与之关联。在您的情况下,没有什么特别的,它只是另一个对象。
SwiftUI提供的绑定来自符合View协议的值类型。这使其与没有值类型的Android区别开来。
MVVM与拥有称为视图模型的对象无关。这是关于模型视图绑定的。
因此,它现在是结构模型:视图,其中带有@State,而不是模型->视图模型->视图层次结构。
全部合为一体,而不是嵌套的3级层次结构。这可能与您以为您对MVVM所了解的一切都不符。实际上,我会说这是一种增强的MVC架构。
但是有绑定。无论您从MVVM绑定中获得什么好处,SwiftUI都可以立即使用它。它只是以一种独特的形式呈现。
正如您所说,即使使用Combine,围绕视图模型进行手动绑定也是一件很麻烦的事情,因为SDK认为到目前为止还没有必要提供这种绑定。 (我对此是否会表示怀疑,因为它是对当前形式的传统MVVM的重大改进)
用于说明以上几点的半伪代码:
struct GameModel {
// build your model
}
struct Game: View {
@State var m = GameModel()
var body: some View {
// access m
}
// actions
func changeCharacter() { // mutate m }
}
注意这有多简单。没有什么比简单性更胜一筹了。甚至没有“ MVVM”。