Je voudrais savoir s'il existe actuellement (au moment de la demande, le premier Xcode 12.0 Beta) un moyen d'initialiser un @StateObject
avec un paramètre provenant d'un initialiseur.
Pour être plus précis, cet extrait de code fonctionne bien:
struct MyView: View { @ObservedObject var myObject: MyObject init(myObject: MyObject) { self.myObject = myObject } }
Mais cela ne fait pas:
struct MyView: View { @StateObject var myObject: MyObject init(id: Int) { self.myObject = MyObject(id: id) } }
D'après ce que je comprends, le rôle de @StateObject
est de faire de la vue le propriétaire de l'objet. La solution de contournement actuelle que j'utilise consiste à transmettre l'instance MyObject déjà initialisée comme suit:
struct MyView: View { @StateObject var myObject = MyObject(id: 1) }
Mais maintenant, pour autant que je sache, la vue qui a créé l'objet le possède, contrairement à cette vue.
Merci.
5 Réponses :
Voici une démonstration de la solution. Testé avec Xcode 12b.
class MyObject: ObservableObject { @Published var id: Int init(id: Int) { self.id = id } } struct MyView: View { @StateObject private var object: MyObject init(id: Int = 1) { _object = StateObject(wrappedValue: MyObject(id: id)) } var body: some View { Text("Test: \(object.id)") } }
Merci beaucoup. Cela fonctionne bien. Cela ressemble cependant à une solution de contournement. Pensez-vous qu'il y a une raison pour laquelle une initialisation standard ne fonctionne pas? Ne suis-je pas censé le faire ou est-ce juste une limitation de ce wrapper de propriété?
Il s'agit à l'origine d'une particularité du wrapper de propriété State. StateObject est exactement le même.
Dans la documentation de @StateObject, il est dit "Vous n'appelez pas cet initialiseur directement".
Vous initialisez l'objet à chaque fois que la vue est reconstruite, ce qui se produit lors de tout changement d'état, cela va certainement à l'encontre de l'objectif de StateObject?
Cette réponse a la mauvaise approche, ne l'utilisez pas! Voir la réponse de Mark pour la bonne manière et même un avertissement d'Apple à propos de cette mauvaise manière.
La réponse donnée par @Asperi doit être évitée Apple le dit dans sa documentation pour StateObject .
Vous n'appelez pas cet initialiseur directement. À la place, déclarez une propriété avec l'attribut @StateObject dans une vue, une application ou une scène et fournissez une valeur initiale.
Apple essaie d'optimiser beaucoup sous le capot, ne combattez pas le système.
Créez simplement un ObservableObject
avec une valeur Published
pour le paramètre que vous vouliez utiliser en premier lieu. Ensuite, utilisez le .onAppear()
pour définir sa valeur et SwiftUI fera le reste.
Code:
class SampleObject: ObservableObject { @Published var id: Int = 0 } struct MainView: View { @StateObject private var sampleObject = SampleObject() var body: some View { Text("Identifier: \(sampleObject.id)") .onAppear() { sampleObject.id = 9000 } } }
La valeur peut être transmise à cette vue, stockée dans une variable et définie sur le sampleObject dans onAppear exactement de la même manière.
Que faire si SampleObject nécessite un paramètre lors de l'initialisation? De plus, .onAppear est un peu désordonné dans SwiftUI 2.0, il peut être appelé plusieurs fois.
J'ai besoin de trouver où j'ai lu ceci, mais je pense que la réponse de @ Asperi est correcte, car les vues init peuvent être appelées plusieurs fois, mais le StateObject n'est pas écrasé, il n'est défini que la première fois.
Je sais qu'il y a déjà une réponse acceptée ici, mais je dois être d'accord avec @malhal sur celle-ci. Je pense que l'init va être appelé plusieurs fois, ce qui est le comportement opposé des intentions @StateObject.
Je n'ai pas vraiment de bonne solution pour @StateObjects pour le moment, mais j'essayais de les utiliser dans l'application @main comme point d'initialisation pour @EnvironmentObjects. Ma solution était de ne pas les utiliser. Je mets cette réponse ici pour les personnes qui essaient de faire la même chose que moi.
J'ai lutté avec cela pendant un certain temps avant de proposer ce qui suit:
Ces deux déclarations let sont au niveau du fichier
private let keychainManager = KeychainManager(service: "com.serious.Auth0Playground") private let authenticatedUser = AuthenticatedUser(keychainManager: keychainManager) @main struct Auth0PlaygroundApp: App { var body: some Scene { WindowGroup { ContentView() .environmentObject(authenticatedUser) } } }
C'est le seul moyen que j'ai trouvé pour initialiser un environmentObject avec un paramètre. Je ne peux pas créer un objet authenticatedUser sans keychainManager et je ne suis pas sur le point de changer l'architecture de toute mon application pour que tous mes objets injectés ne prennent pas de paramètre.
Je suppose que j'ai trouvé une solution de contournement pour pouvoir contrôler l'instanciation d'un modèle de vue enveloppé avec @StateObject. Si vous ne rendez pas le modèle de vue privé sur la vue, vous pouvez utiliser l'initialisation par membre synthétisée, et là vous pourrez contrôler l'instanciation de celui-ci sans problème. Si vous avez besoin d'un moyen public pour instancier votre vue, vous pouvez créer une méthode de fabrique qui reçoit vos dépendances de modèle de vue et utilise l'initialisation synthétisée interne.
import SwiftUI class MyViewModel: ObservableObject { @Published var message: String init(message: String) { self.message = message } } struct MyView: View { @StateObject var viewModel: MyViewModel var body: some View { Text(viewModel.message) } } public func myViewFactory(message: String) -> some View { MyView(viewModel: .init(message: message)) }
Je ne vois pas où viewModel est initialisé dans l'exemple ci-dessus, pouvez-vous élaborer un peu plus, ou ajouter du code manquant s'il manque?
Voir à l'intérieur de la func myViewFactory
Oui, j'ai vu ça, mais où s'appelle myViewFactory?
Vous appellerez myViewFactory(params)
partout où vous appelleriez MyView(params)
Comme @Mark l'a souligné, vous ne devez manipuler @StateObject
nulle part pendant l'initialisation. C'est parce que @StateObject
est initialisé après View.init () et légèrement avant / après l'appel du corps.
J'ai essayé de nombreuses approches différentes sur la façon de transmettre des données d'une vue à une autre et j'ai trouvé une solution qui convient aux modèles de vues / vues simples et complexes.
Version
class SubviewModel: ObservableObject { @Published var subViewText: String? func updateText(text: String?) { self.subViewText = text } }
Cette solution fonctionne avec iOS 14.0 vers le haut, car vous avez besoin du modificateur de vue .onChange()
. L'exemple est écrit dans Swift Playgrounds. Si vous avez besoin d' un onChange
comme modificateur pour les versions inférieures, vous devez écrire votre propre modificateur.
Vue principale
La vue principale a un @StateObject viewModel
gérant toute la logique des vues, comme le @StateObject viewModel
sur le bouton et les "données" (testingID: String)
-> Vérifiez le ViewModel
.onAppear(perform: { self.viewModel.updateText(text: test) })
Modèle de vue principale (ViewModel)
Le viewModel publie un testID: String?
. Ce testID peut être n'importe quel type d'objet (par exemple, un objet de configuration, vous le nommez), pour cet exemple, il s'agit simplement d'une chaîne également nécessaire dans la vue secondaire.
.onChange(of: self.test) { (text) in self.viewModel.updateText(text: text) }
Ainsi, en appuyant sur le bouton, notre ViewModel
mettra à jour le testID
. Nous voulons également ce testID
dans notre SubView
et s'il change, nous voulons également que notre SubView
reconnaisse et gère ces changements. Grâce au ViewModel @Published var testingID
nous sommes en mesure de publier des modifications dans notre vue. Jetons maintenant un œil à notre SubView et SubViewModel .
Sous-vue
Ainsi, le SubView
a son propre @StateObject
pour gérer sa propre logique. Il est complètement séparé des autres vues et ViewModels. Dans cet exemple, le SubView
présente uniquement le testID de sa MainView
. Mais rappelez-vous, il peut s'agir de tout type d'objet comme des préréglages et des configurations pour une requête de base de données.
struct SubView: View { @StateObject var viewModel: SubviewModel = .init() @Binding var test: String? init(text: Binding<String?>) { self._test = text } var body: some View { Text(self.viewModel.subViewText ?? "no text") .onChange(of: self.test) { (text) in self.viewModel.updateText(text: text) } .onAppear(perform: { self.viewModel.updateText(text: test) }) } }
Pour "connecter" notre testingID
publié par notre MainViewModel
nous initialisons notre SubView
avec un @Binding
. Nous avons donc maintenant le même testingID
dans notre SubView
. Mais nous ne voulons pas l'utiliser directement dans la vue, nous devons plutôt passer les données dans notre SubViewModel
, rappelez-vous que notre SubViewModel est un @StateObject
pour gérer toute la logique. Et nous ne pouvons pas passer la valeur dans notre @StateObject
lors de l'initialisation de la vue, comme je l'ai écrit au début. De plus, si les données ( testingID: String
) changent dans notre MainViewModel
, notre SubViewModel
doit reconnaître et gérer ces changements.
Par conséquent, nous utilisons deux ViewModifiers
.
sur le changement
final class ViewModel: ObservableObject { @Published var testingID: String? func didTapButton() { self.testingID = UUID().uuidString } }
Le modificateur onChange est abonnée à des changements dans notre @Binding
propriété. Donc, si cela change , ces changements sont transmis à notre SubViewModel
. Notez que votre propriété doit être équatable . Si vous passez un objet plus complexe, comme un Struct
, assurez-vous d' implémenter ce protocole dans votre Struct
.
onAppear
Nous avons besoin de onAppear
pour gérer les "premières données initiales" car onChange ne se déclenche pas la première fois que votre vue est initialisée. Ce n'est que pour les changements .
struct TestMainView: View { @StateObject var viewModel: ViewModel = .init() var body: some View { VStack { Button(action: { self.viewModel.didTapButton() }) { Text("TAP") } Spacer() SubView(text: $viewModel.testingID) }.frame(width: 300, height: 400) } }
Ok et voici le SubViewModel , rien de plus à expliquer à celui-ci je suppose.
Apple Swift version 5.3.1 (swiftlang-1200.0.41 clang-1200.0.32.8)
Maintenant , vos données sont synchronisées entre votre MainViewModel et SubViewModel et cette approche fonctionne pour les grandes vues avec de nombreux sous - vues et subviews de ces sous - vues et ainsi de suite. Il conserve également vos vues et les viewModels correspondants inclus avec une réutilisabilité élevée.
Exemple de travail
Playground sur GitHub: https://github.com/luca251117/PassingDataBetweenViewModels
Notes complémentaires
Pourquoi j'utilise onAppear
et onChange
au lieu de seulement onReceive
: Il semble que le remplacement de ces deux modificateurs avec onReceive
conduit à un flux continu de données mise à feu des SubViewModel updateText
plusieurs fois. Si vous avez besoin de diffuser des données pour la présentation, cela peut être bien, mais si vous souhaitez gérer des appels réseau par exemple, cela peut entraîner des problèmes. C'est pourquoi je préfère "l'approche à deux modificateurs".
Note personnelle: veuillez ne pas modifier le stateObject en dehors de la portée de la vue correspondante. Même si c'est possible d'une manière ou d'une autre, ce n'est pas ce à quoi cela sert.