J'essaie d'utiliser MVVM dans une application SwiftUI, mais il semble que les modèles de vue pour les vues enfants (par exemple, ceux d'un NavigationLink
) soient réinitialisés chaque fois qu'un ObservableObject
observé par le parent et l'enfant est mis à jour. Cela entraîne la réinitialisation de l'état local de l'enfant, le rechargement des données réseau, etc.
Je suppose que c'est parce que cela provoque la réévaluation du body
du parent, qui contient un constructeur du modèle de vue de SubView
, mais je n'ai pas été en mesure de trouver une alternative qui me permette de créer des modèles de vue qui ne vivent pas au-delà de la vie de la vue. Je dois être en mesure de transmettre des données au modèle de vue enfant du parent.
Voici un terrain de jeu très simplifié de ce que nous essayons d'accomplir, où l'incrémentation d' EnvCounter.counter
réinitialise SubView.counter
.
import SwiftUI import PlaygroundSupport class EnvCounter: ObservableObject { @Published var counter = 0 } struct ContentView: View { @ObservedObject var envCounter = EnvCounter() var body: some View { VStack { Text("Parent view") Button(action: { self.envCounter.counter += 1 }) { Text("EnvCounter is at \(self.envCounter.counter)") } .padding(.bottom, 40) SubView(viewModel: .init()) } .environmentObject(envCounter) } } struct SubView: View { class ViewModel: ObservableObject { @Published var counter = 0 } @EnvironmentObject var envCounter: EnvCounter @ObservedObject var viewModel: ViewModel var body: some View { VStack { Text("Sub view") Button(action: { self.viewModel.counter += 1 }) { Text("SubView counter is at \(self.viewModel.counter)") } Button(action: { self.envCounter.counter += 1 }) { Text("EnvCounter is at \(self.envCounter.counter)") } } } } PlaygroundPage.current.setLiveView(ContentView())
3 Réponses :
J'avais le même problème, vos suppositions sont justes, SwiftUI calcule tout votre corps parent à chaque fois que son état change. La solution consiste à déplacer l'init ViewModel enfant vers le ViewModel du parent, voici le code de votre exemple:
class EnvCounter: ObservableObject { @Published var counter = 0 @Published var subViewViewModel = SubView.ViewModel.init() } struct CounterView: View { @ObservedObject var envCounter = EnvCounter() var body: some View { VStack { Text("Parent view") Button(action: { self.envCounter.counter += 1 }) { Text("EnvCounter is at \(self.envCounter.counter)") } .padding(.bottom, 40) SubView(viewModel: envCounter.subViewViewModel) } .environmentObject(envCounter) } }
Un nouveau wrapper de propriété est ajouté à SwiftUI dans Xcode 12, @StateObject. Votre problème devrait être résolu avec.
Cela ne répond pas à la question. Une fois que vous aurez une réputation suffisante, vous pourrez commenter n'importe quel message ; à la place, fournissez des réponses qui ne nécessitent pas de clarification de la part du demandeur . - De l'avis
Pour résoudre ce problème, j'ai créé une classe d'assistance personnalisée appelée ViewModelProvider
.
Le fournisseur prend un hachage pour votre vue et une méthode qui génère le ViewModel. Il renvoie ensuite le ViewModel ou le construit si c'est la première fois qu'il reçoit ce hachage.
Tant que vous vous assurez que le hachage reste le même tant que vous voulez le même ViewModel, cela résout le problème.
Struct MyView: View { @ObservedObject var viewModel: MyViewModel public init(thisParameterDoesntChangeVM: String, thisParameterChangesVM: String) { self.viewModel = ViewModelProvider.makeViewModel(forHash: thisParameterChangesVM) { MOFOnboardingFlowViewModel( pages: pages, baseStyleConfig: style, buttonConfig: buttonConfig, onFinish: onFinish ) } } }
Ensuite, dans votre View, vous pouvez utiliser le ViewModel:
class ViewModelProvider { private static var viewModelStore = [String:Any]() static func makeViewModel<VM>(forHash hash: String, usingBuilder builder: () -> VM) -> VM { if let vm = viewModelStore[hash] as? VM { return vm } else { let vm = builder() viewModelStore[hash] = vm return vm } } }
Dans cet exemple, il y a deux paramètres. Seul thisParameterChangesVM
est utilisé dans le hachage. Cela signifie que même si thisParameterDoesntChangeVM
change et que la vue est reconstruite, le modèle de vue reste le même.
Je suis allé de la même manière mais au lieu de [String: Any], utilisez NSMapTable <NSString, AnyObject> (keyOptions: NSPointerFunctions.Options.strongMemory, valueOptions: NSPointerFunctions.Options.weakMemory) pour supprimer viewModel à la vue disparaît.
C'est un comportement normal. Lors de l'actualisation, la propriété calculable
View.body
est appelée, de sorte que tout code à l'intérieur, non masqué par des conditions d'état internes, est exécuté, de sorte que tous les constructeurs de vue visibles sont appelés. Ne mettez rien de lourd dans les constructeurs de vue et / ou dans les valeurs par défaut des propriétés, déplacez toute cette logique hors de la vue (il y aura un bonus - un rendu rapide de l'interface utilisateur).