21
votes

Initialiser @StateObject avec un paramètre dans SwiftUI

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.


0 commentaires

5 Réponses :


7
votes

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


5 commentaires

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.



13
votes

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


3 commentaires

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.



0
votes

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.


0 commentaires

0
votes

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


4 commentaires

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)



0
votes

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.


0 commentaires