Afin d'obtenir une apparence et une sensation propres du code de l'application, je crée des ViewModels pour chaque vue qui contient une logique.
Un ViewModel normal ressemble un peu à ceci:
class SomeViewModel: ObservableObject {
@Published var state = 1
// Logic and calls of Business Logic goes here
}
et est utilisé comme ceci:
struct SomeView: View {
@ObservedObject var viewModel = SomeViewModel()
var body: some View {
// Code to read and write the State goes here
}
}
Cela fonctionne correctement lorsque le parent de vues n'est pas mis à jour. Si l'état du parent change, cette vue est redessinée (assez normal dans un framework déclaratif). Mais également le ViewModel est recréé et ne détient pas l'état par la suite. Ceci est inhabituel lorsque vous comparez à d'autres Frameworks (par exemple: Flutter).
À mon avis, le ViewModel devrait rester, ou l'état devrait persister.
Si je remplace le ViewModel par un @State
Propriété et utilisez la int
(dans cet exemple) directement, elle reste persistante et ne se recrée pas:
struct SomeView: View {
@State var state = 1
var body: some View {
// Code to read and write the State goes here
}
}
Cela ne fonctionne évidemment pas pour les États plus complexes. Et si je définis une classe pour @State
(comme le ViewModel) de plus en plus Les choses ne fonctionnent pas comme prévu.
@State
Propertywrapper pour @ObservedObject
?Je sais qu'habituellement, il est mauvais de créer un ViewModel dans une vue intérieure, mais ce comportement peut être répliqué à l'aide d'un NavigationLink ou d'une feuille.
Parfois, il n'est alors tout simplement pas utile de conserver l'état dans ParentsViewModel et de travailler avec des liaisons lorsque vous pensez à un TableView très complexe, où les cellules elles-mêmes contiennent beaucoup de logique.
Il existe toujours une solution de contournement pour les cas individuels, mais je pense que ce serait beaucoup plus facile si le ViewModel ne serait pas recréé.
Je sais qu'il y a beaucoup de questions sur ce problème, toutes concernant des cas d'utilisation très spécifiques. Ici, je veux parler du problème général, sans aller trop loin dans les solutions personnalisées.
Lorsque vous avez un ParentView à changement d'état, comme une liste provenant d'une base de données, d'une API ou d'un cache (pensez à quelque chose de simple). Via un NavigationLink
vous pouvez atteindre une page de détail où vous pouvez modifier les données. En changeant les données, le Pattern réactif/déclaratif nous dirait de mettre également à jour le ListView, ce qui "redessinerait" le NavigationLink
, ce qui conduirait alors à une recréation du ViewModel.
Je sais que je pourrais stocker le ViewModel dans le ViewModel de ParentView/ParentView, mais c'est la mauvaise façon de le faire IMO. Et comme les abonnements sont détruits et/ou recréés, il peut y avoir des effets secondaires.
Enfin, il existe une solution fournie par Apple: @StateObject
.
En remplaçant @ObservedObject
avec @StateObject
tout ce qui est mentionné dans mon message initial fonctionne.
Malheureusement, cela n'est disponible que dans iOS 14+.
Ceci est mon code de Xcode 12 Beta (Publié le 23 juin 2020)
struct ContentView: View {
@State var title = 0
var body: some View {
NavigationView {
VStack {
Button("Test") {
self.title = Int.random(in: 0...1000)
}
TestView1()
TestView2()
}
.navigationTitle("\(self.title)")
}
}
}
struct TestView1: View {
@ObservedObject var model = ViewModel()
var body: some View {
VStack {
Button("Test1: \(self.model.title)") {
self.model.title += 1
}
}
}
}
class ViewModel: ObservableObject {
@Published var title = 0
}
struct TestView2: View {
@StateObject var model = ViewModel()
var body: some View {
VStack {
Button("StateObject: \(self.model.title)") {
self.model.title += 1
}
}
}
}
Comme vous pouvez le voir, le StateObject
conserve sa valeur lors du rafraîchissement de la vue parent, pendant que le ObservedObject
est en cours de réinitialisation.
Je suis d'accord avec vous, je pense que c'est l'un des nombreux problèmes majeurs avec SwiftUI. Voici ce que je me retrouve à faire, aussi dégoûtant soit-il.
struct MyView: View {
@State var viewModel = MyViewModel()
var body : some View {
MyViewImpl(viewModel: viewModel)
}
}
fileprivate MyViewImpl : View {
@ObservedObject var viewModel : MyViewModel
var body : some View {
...
}
}
Vous pouvez soit construire le modèle de vue sur place, soit le transmettre, et cela vous donne une vue qui maintiendra votre ObservableObject tout au long de la reconstruction.
Existe-t-il un moyen de ne pas recréer le ViewModel à chaque fois?
Oui, conservez l'instance de ViewModel extérieur of SomeView
et injectez via le constructeur
struct SomeView: View {
@ObservedObject var viewModel: SomeViewModel // << only declaration
Existe-t-il un moyen de répliquer le @State Propertywrapper pour @ObservedObject?
Aucun besoin. @ObservedObject
est-a déjà DynamicProperty
de la même manière que @State
Pourquoi @State garde-t-il l'état sur le redessiner?
Parce qu'il garde son stockage, c'est à dire. valeur enveloppée, extérieur de la vue. (donc, voir à nouveau le premier ci-dessus)
Vous devez fournir des PassThroughSubject
personnalisés dans votre classe ObservableObject
. Regardez ce code:
//
// Created by Франчук Андрей on 08.05.2020.
// Copyright © 2020 Франчук Андрей. All rights reserved.
//
import SwiftUI
import Combine
struct TextChanger{
var textChanged = PassthroughSubject<String,Never>()
public func changeText(newValue: String){
textChanged.send(newValue)
}
}
class ComplexState: ObservableObject{
var objectWillChange = ObservableObjectPublisher()
let textChangeListener = TextChanger()
var text: String = ""
{
willSet{
objectWillChange.send()
self.textChangeListener.changeText(newValue: newValue)
}
}
}
struct CustomState: View {
@State private var text: String = ""
let textChangeListener: TextChanger
init(textChangeListener: TextChanger){
self.textChangeListener = textChangeListener
print("did init")
}
var body: some View {
Text(text)
.onReceive(textChangeListener.textChanged){newValue in
self.text = newValue
}
}
}
struct CustomStateContainer: View {
//@ObservedObject var state = ComplexState()
var state = ComplexState()
var body: some View {
VStack{
HStack{
Text("custom state View: ")
CustomState(textChangeListener: state.textChangeListener)
}
HStack{
Text("ordinary Text View: ")
Text(state.text)
}
HStack{
Text("text input: ")
TextInput().environmentObject(state)
}
}
}
}
struct TextInput: View {
@EnvironmentObject var state: ComplexState
var body: some View {
TextField("input", text: $state.text)
}
}
struct CustomState_Previews: PreviewProvider {
static var previews: some View {
return CustomStateContainer()
}
}
Tout d'abord, j'utilise TextChanger
pour passer la nouvelle valeur de .text
À .onReceive(...)
dans CustomState
View. Notez que onReceive
dans ce cas obtient PassthroughSubject
, pas le ObservableObjectPublisher
. Dans le dernier cas, vous n'aurez que Publisher.Output
Dans perform: closure
, Pas la NewValue. state.text
Dans ce cas aurait une ancienne valeur.
Deuxièmement, regardez la classe ComplexState
. J'ai créé une propriété objectWillChange
pour que les modifications de texte envoient manuellement une notification aux abonnés. C'est presque la même chose que le wrapper @Published
. Mais, lorsque le texte change, il enverra à la fois, et objectWillChange.send()
et textChanged.send(newValue)
. Cela vous permet de choisir exactement View
, comment réagir au changement d'état. Si vous voulez un comportement ordinaire, mettez simplement l'état dans le wrapper @ObservedObject
Dans CustomStateContainer
View. Ensuite, vous aurez toutes les vues recréées et cette section obtiendra également des valeurs mises à jour:
HStack{
Text("ordinary Text View: ")
Text(state.text)
}
Si vous ne voulez pas qu'ils soient tous recréés, supprimez simplement @ObservedObject. La vue de texte ordinaire cessera de se mettre à jour, mais CustomState le fera. Sans recréation.
mise à jour: si vous voulez plus de contrôle, vous pouvez décider lors de la modification de la valeur, qui voulez-vous informer de ce changement. Vérifiez le code plus complexe:
//
//
// Created by Франчук Андрей on 08.05.2020.
// Copyright © 2020 Франчук Андрей. All rights reserved.
//
import SwiftUI
import Combine
struct TextChanger{
// var objectWillChange: ObservableObjectPublisher
// @Published
var textChanged = PassthroughSubject<String,Never>()
public func changeText(newValue: String){
textChanged.send(newValue)
}
}
class ComplexState: ObservableObject{
var onlyPassthroughSend = false
var objectWillChange = ObservableObjectPublisher()
let textChangeListener = TextChanger()
var text: String = ""
{
willSet{
if !onlyPassthroughSend{
objectWillChange.send()
}
self.textChangeListener.changeText(newValue: newValue)
}
}
}
struct CustomState: View {
@State private var text: String = ""
let textChangeListener: TextChanger
init(textChangeListener: TextChanger){
self.textChangeListener = textChangeListener
print("did init")
}
var body: some View {
Text(text)
.onReceive(textChangeListener.textChanged){newValue in
self.text = newValue
}
}
}
struct CustomStateContainer: View {
//var state = ComplexState()
@ObservedObject var state = ComplexState()
var body: some View {
VStack{
HStack{
Text("custom state View: ")
CustomState(textChangeListener: state.textChangeListener)
}
HStack{
Text("ordinary Text View: ")
Text(state.text)
}
HStack{
Text("text input with full state update: ")
TextInput().environmentObject(state)
}
HStack{
Text("text input with no full state update: ")
TextInputNoUpdate().environmentObject(state)
}
}
}
}
struct TextInputNoUpdate: View {
@EnvironmentObject var state: ComplexState
var body: some View {
TextField("input", text: Binding( get: {self.state.text},
set: {newValue in
self.state.onlyPassthroughSend.toggle()
self.state.text = newValue
self.state.onlyPassthroughSend.toggle()
}
))
}
}
struct TextInput: View {
@State private var text: String = ""
@EnvironmentObject var state: ComplexState
var body: some View {
TextField("input", text: Binding(
get: {self.text},
set: {newValue in
self.state.text = newValue
// self.text = newValue
}
))
.onAppear(){
self.text = self.state.text
}.onReceive(state.textChangeListener.textChanged){newValue in
self.text = newValue
}
}
}
struct CustomState_Previews: PreviewProvider {
static var previews: some View {
return CustomStateContainer()
}
}
J'ai créé une liaison manuelle pour arrêter la diffusion de objectWillChange. Mais vous devez toujours obtenir une nouvelle valeur à tous les endroits où vous modifiez cette valeur pour rester synchronisé. C'est pourquoi j'ai aussi modifié TextInput.
C'est ce dont vous aviez besoin?