web-dev-qa-db-ja.com

SwiftUIとMVVM-モデルとビューモデル間の通信

私は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?

どういう意味かはっきりしているといいですね。わかりにくいので説明するのは難しい

ありがとう!

7
Quantm

上記のサンプルコードを投稿してくれたQuantmに感謝します。私はあなたの例に従いましたが、少し単純化しました。私が行った変更:

  • Combineを使用する必要はありません
  • ビューモデルとビューの間の唯一の接続は、SwiftUIが提供するバインディングです。例:@Published(ビューモデル内)と@ObservedObject(ビュー内)のペアを使用します。ビューモデルを使用して複数のビューにまたがるバインディングを構築する場合は、@ Publishedと@EnvironmentObjectのペアを使用することもできます。

これらの変更により、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()
      }
   }

0
lq_msu

Viewの_@Observed_変数に警告するには、objectWillChange

_PassthroughSubject<Void, Never>()
_

また、

_objectWillChange.send()
_

changeCharacter()関数内。

0
Ken Mueller

短い答えは、@ 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」すらありません。

0
Jim lai