@StateObject est un Property Wrapper dans SwiftUI permettant de créer et de posséder une instance ObservableObject directement dans une vue. SwiftUI garantit que l’objet est initialisé une fois par cycle de vie de la vue et n’est pas recréé lors des rendus ultérieurs. Selon la Documentation Développeur Apple (2025), @StateObject est recommandé pour les vues racine qui créent une source de données. @StateObject est le bon choix pour posséder un ObservableObject dans la hiérarchie SwiftUI.
Points clés
@StateObject est un Property Wrapper introduit dans SwiftUI 2.0 (iOS 14) qui combine les capacités de @ObservedObject et @State. Comme @ObservedObject, il s’abonne aux changements d’ObservableObject. Comme @State, il garantit que les données survivent aux initialisations répétées de la structure de la vue. @StateObject crée l’objet une fois lorsque la vue apparaît pour la première fois à l’écran et le stocke dans le tas SwiftUI.
Avant @StateObject, les développeurs utilisaient @ObservedObject pour tous les ObservableObjects, y compris ceux créés dans les vues. Cela entraînait des pertes de données fréquentes lorsque la vue parente était mise à jour, ce qui provoquait la recréation de la structure de la vue et emportait l’instance @ObservedObject avec elle. @StateObject a résolu ce problème en ajoutant une garantie de stabilité.
La règle principale : @StateObject est utilisé dans la vue qui crée l’objet dans l’initialiseur par défaut (let model = ViewModel()). Les vues enfants qui reçoivent cet objet utilisent @ObservedObject. Cette séparation garantit une source de vérité unique dans toute la hiérarchie.
SwiftUI gère le cycle de vie de @StateObject via un gestionnaire de stockage similaire à @State. Lorsque la vue apparaît pour la première fois, SwiftUI alloue de la mémoire pour l’objet et le stocke dans une zone persistante. Lors des rendus ultérieurs (appels body), l’objet n’est pas recréé—l’instance existante est utilisée. L’objet vit tant que la vue est dans la hiérarchie.
Lorsque la vue est supprimée de la hiérarchie, SwiftUI détruit le @StateObject, appelant deinit. Lorsque la vue est rajoutée à la hiérarchie, une nouvelle instance est créée. Ceci est important à prendre en compte lors de la conception : si vous devez conserver des données entre les suppressions de vues, utilisez une couche de service (singleton ou DI) ou @AppStorage pour la persistance.
class TimerViewModel: ObservableObject {
@Published var seconds: Int = 0
private var timer: Timer?
func start() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
self.seconds += 1
}
}
deinit {
timer?.invalidate()
}
}
struct TimerView: View {
@StateObject var viewModel = TimerViewModel()
var body: some View {
Text("\(viewModel.seconds)s")
.onAppear { viewModel.start() }
}
}
Dans l’exemple, TimerViewModel est créé via @StateObject et vit tant que TimerView est à l’écran. Le timer démarre dans onAppear et s’arrête dans deinit. Si @ObservedObject était utilisé, chaque rendu de TimerView créerait un nouveau TimerViewModel avec secondes = 0, et le timer ne fonctionnerait jamais correctement. @StateObject garantit que le viewModel est unique et stable.
Le choix entre @StateObject et @ObservedObject dépend de qui possède l’objet. Si la vue crée l’objet—@StateObject. Si la vue reçoit un objet déjà créé—@ObservedObject. Cette règle est si importante que Xcode affiche un avertissement lors de l’utilisation de @StateObject dans une vue enfant qui reçoit l’objet via un initialiseur.
| Situation | Wrapper recommandé |
|---|---|
| La vue crée le modèle via ViewModel() | @StateObject |
| La vue reçoit le modèle du parent | @ObservedObject |
| Le modèle est utilisé dans une vue | @StateObject |
| Le modèle est passé via Environment | @EnvironmentObject |
| Le modèle est nécessaire pour les aperçus | @ObservedObject + mock |
En pratique, au début d’un projet, @StateObject est souvent utilisé dans la vue racine et @ObservedObject dans toutes les vues enfants. Au fur et à mesure que l’application grandit, certaines instances @StateObject peuvent être remplacées par @EnvironmentObject pour simplifier la hiérarchie. Cependant, @StateObject reste le meilleur choix pour les écrans modulaires avec leur propre logique.
Le premier modèle—MVVM avec @StateObject. Le ViewModel en tant qu’ObservableObject est créé dans la vue via @StateObject. Le ViewModel contient des propriétés @Published et la logique métier. La vue s’abonne aux changements et met à jour l’interface. Cette approche offre un isolement testable : le ViewModel peut être testé sans UI en créant une instance directement.
Le deuxième modèle—@StateObject avec dépendances. Si le ViewModel nécessite des services, utilisez l’initialisation avec paramètres. Par exemple, @StateObject var viewModel = UserViewModel(api: APIClient.shared). Attention cependant : les paramètres sont calculés à chaque rendu de body, mais l’objet n’est créé qu’une seule fois. SwiftUI ignore les initialisations ultérieures de @StateObject.
Le troisième modèle—@StateObjects imbriqués. Dans SwiftUI, vous pouvez avoir plusieurs @StateObjects dans une vue, mais c’est rarement justifié. Habituellement, un @StateObject gère l’ensemble des données de la vue. Si la logique devient trop complexe, divisez-la en une composition de services @ObservedObject au sein d’un @StateObject.
struct AppView: View {
@StateObject var router = NavigationRouter()
@StateObject var auth = AuthViewModel()
var body: some View {
ContentView()
.environmentObject(router)
.environmentObject(auth)
}
}
Dans l’exemple, AppView crée deux @StateObjects : NavigationRouter pour gérer la navigation et AuthViewModel pour l’authentification. Les deux objets sont injectés dans l’Environment via environmentObject. Toute vue enfant peut y accéder via @EnvironmentObject sans passer par la chaîne d’initialiseurs.
@StateObject prend en charge l’initialisation avec n’importe quels paramètres, mais avec une réserve importante : l’initialiseur n’est appelé qu’une seule fois. Lors des rendus body ultérieurs, les nouvelles valeurs des paramètres sont ignorées. Cela signifie que si vous passez @State var id: Int = 5 à @StateObject var vm = ViewModel(id: id), lorsque id change, le ViewModel ne recevra pas la nouvelle valeur.
Pour résoudre ce problème, utilisez onReceive ou onAppear pour la synchronisation. Abonnez-vous aux changements de paramètres dans le ViewModel via Combine ou transmettez les paramètres via la méthode .onChange(of:) au niveau de la vue. Une alternative est d’utiliser @ObservedObject au lieu de @StateObject si l’objet doit réagir dynamiquement aux changements externes.
struct DetailView: View {
let itemId: Int
@StateObject var viewModel = DetailViewModel()
var body: some View {
Text(viewModel.title)
.onAppear { viewModel.load(id: itemId) }
}
}
L’approche correcte : DetailView reçoit itemId comme propriété let (transmise via l’initialiseur de la structure), et @StateObject crée DetailViewModel sans paramètres. Dans onAppear, la méthode load(id:) est appelée pour charger les données pour l’ID transmis. Cela garantit que le ViewModel est créé par le mécanisme @StateObject, mais les données sont chargées à chaque apparition de la vue avec l’ID actuel.
L’erreur principale—utiliser @StateObject dans les vues enfants qui reçoivent l’objet d’un parent. Si ParentView crée @StateObject model, et que ChildView déclare @StateObject var model: ModelType (avec un paramètre par défaut), ChildView créera sa propre instance indépendante. Les objets parent et enfant ne seront pas connectés, et les changements dans l’un ne se refléteront pas dans l’autre.
La deuxième erreur—placer @StateObject dans List ou ForEach. Chaque élément de la liste crée son propre @StateObject, ce qui conduit à de multiples instances indépendantes. Pour les listes, l’approche correcte est de passer un seul ObservableObject à tous les éléments via @ObservedObject ou d’utiliser des structures Identifiable avec @State dans List.
Le troisième problème—absence de nettoyage dans deinit. @StateObject vit pendant tout le cycle de vie de la vue. Si l’objet crée des timers, des abonnements Combine ou des requêtes réseau, deinit doit les annuler. Sinon, les fuites mémoire et la poursuite du travail en arrière-plan après la fermeture de l’écran sont inévitables. Utilisez toujours un stockage Cancellable Combine ou invalidez les timers dans deinit.
Questions fréquentes
@StateObject a été ajouté dans SwiftUI 2.0 à la WWDC 2020 avec iOS 14, macOS 11, watchOS 7 et tvOS 14. Avant cela, @ObservedObject était le seul moyen de travailler avec ObservableObject, ce qui entraînait fréquemment des bogues de perte de données.
Non, @StateObject ne prend pas en charge les types Optionnel. L’objet doit être initialisé lors de la déclaration. Si vous avez besoin d’un objet optionnel, utilisez @ObservedObject ou @EnvironmentObject avec un type optionnel.
Ajoutez print(#function) à l’initialiseur et au deinit de l’ObservableObject. Si init n’est pas appelé lors des rendus—@StateObject fonctionne correctement. Si init est appelé à chaque fois—remplacez @ObservedObject par @StateObject.
Oui, @StateObject fonctionne dans les vues SwiftUI intégrées dans UIKit via UIHostingController. Le cycle de vie de l’objet est lié à la vue SwiftUI, pas au UIViewController. Si la vue SwiftUI est remplacée, le @StateObject est détruit.
Plusieurs petits @StateObjects avec des responsabilités séparées. Cela améliore la testabilité, la réutilisabilité et les performances—lorsqu’un objet change, seules les parties abonnées de l’interface sont redessinées, pas la vue entière.
Résumé
Nous développerons une application mobile clé en main
IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.
Lisez aussi