5
votes

SwiftUI MVVM: modèle de vue enfant réinitialisé lors de la mise à jour de la vue parent

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())


1 commentaires

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).


3 Réponses :


0
votes

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


0 commentaires

2
votes

Un nouveau wrapper de propriété est ajouté à SwiftUI dans Xcode 12, @StateObject. Votre problème devrait être résolu avec.


1 commentaires

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



4
votes

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.


1 commentaires

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.