私はSwiftUI
で使用されているMVVMモデルを実験してきましたが、まだ十分に理解していないものがあります。
SwiftUI
は@ObservableObject
/@ObservedObject
を使用して、body
プロパティの再計算をトリガーし、ビューを更新するビューモデルの変更を検出します。
MVVMモデルでは、これがビューとビューモデル間の通信です。私がよく理解していないのは、モデルとビューモデルの通信方法です。
モデルが変更された場合、ビューモデルはどのようにそれを認識しますか?新しいCombine
フレームワークを手動で使用して、ビューモデルがサブスクライブできるモデル内にパブリッシャーを作成することを考えました。
しかし、私はこのアプローチをかなり退屈にする単純な例を作成したと思います。 Game.Character
オブジェクトの配列を保持するGame
というモデルがあります。キャラクターには、変更可能な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?
どういう意味かはっきりしているといいですね。わかりにくいので説明するのは難しい
ありがとう!
上記のサンプルコードを投稿してくれた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()
}
}
View
の_@Observed
_変数に警告するには、objectWillChange
を
_PassthroughSubject<Void, Never>()
_
また、
_objectWillChange.send()
_
changeCharacter()
関数内。
短い答えは、@ Stateを使用することです。stateプロパティが変更されるたびに、ビューが再構築されます。
長い答えは、SwiftUIごとにMVVMパラダイムを更新することです。
通常、何かが「ビューモデル」になるためには、いくつかのバインディングメカニズムを関連付ける必要があります。あなたの場合、それについて特別なことは何もありません、それは単なる別のオブジェクトです。
SwiftUIによって提供されるバインディングは、Viewプロトコルに準拠した値型からのものです。これは、Android値タイプがない場合とは異なります。
MVVMは、ビューモデルと呼ばれるオブジェクトを持つことではありません。それは、モデルとビューのバインディングを持つことです。
したがって、モデル->モデルの表示->ビューの階層の代わりに、@ Stateを内部に持つstruct Model:Viewになります。
ネストされた3レベルの階層ではなく、オールインワン。それは、MVVMについて知っていると思ったすべてに反する可能性があります。実際、これは拡張MVCアーキテクチャだと思います。
しかし、拘束力はあります。 MVVMバインディングから得られるメリットが何であれ、SwiftUIにはすぐに使用できます。それはただユニークな形で現れます。
あなたが述べたように、SDKはまだそのようなバインディングを提供する必要がないと見なしているため、Combineを使用してもビューモデルの周りに手動バインディングを行うのは面倒です。 (現在の形式の従来の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」すらありません。