# SwiftUI @Observable vs @State : quand utiliser quoi en 2026 > Comprendre les différences entre @Observable et @State en SwiftUI pour choisir le bon outil de gestion d'état selon le contexte de votre application iOS. - Published: 2026-02-25 - Updated: 2026-03-28 - Author: SharpSkill - Tags: swiftui, ios, observable, state-management, swift - Reading time: 14 min --- La gestion d'état constitue le pilier central de toute application SwiftUI performante. Depuis iOS 17, le macro `@Observable` révolutionne la façon de créer des modèles réactifs, tandis que `@State` reste incontournable pour l'état local des vues. Comprendre quand utiliser chaque outil permet d'éviter les re-renders inutiles et de construire des applications fluides. > **Ce que couvre cet article** > > Cet article explore les mécanismes internes de `@Observable` et `@State`, leurs différences fondamentales, et fournit des règles claires pour choisir le bon outil selon le contexte. ## Les fondamentaux de @State `@State` représente la forme la plus simple de gestion d'état en SwiftUI. Ce property wrapper crée un stockage persistant pour une valeur qui appartient exclusivement à la vue qui la déclare. ```swift // CounterView.swift struct CounterView: View { // @State crée un stockage géré par SwiftUI @State private var count = 0 var body: some View { VStack(spacing: 20) { // La vue se met à jour quand count change Text("Compteur : \(count)") .font(.largeTitle) HStack(spacing: 16) { Button("- 1") { count -= 1 } Button("+ 1") { count += 1 } } .buttonStyle(.borderedProminent) } } } ``` Chaque modification de `count` déclenche un re-render de la vue. SwiftUI gère automatiquement le cycle de vie de cette valeur, la préservant lors des reconstructions du body. ### Caractéristiques clés de @State `@State` possède plusieurs propriétés distinctives qui définissent son utilisation optimale : ```swift // StateCharacteristics.swift struct FormView: View { // ✅ État local simple - types valeur @State private var username = "" @State private var isEnabled = true @State private var selectedIndex = 0 // ✅ Types valeur complexes supportés @State private var configuration = FormConfiguration() var body: some View { Form { TextField("Nom d'utilisateur", text: $username) Toggle("Activé", isOn: $isEnabled) Picker("Option", selection: $selectedIndex) { Text("Option A").tag(0) Text("Option B").tag(1) Text("Option C").tag(2) } } } } // Les structs fonctionnent parfaitement avec @State struct FormConfiguration: Equatable { var theme: Theme = .light var fontSize: CGFloat = 16 var showNotifications: Bool = true } enum Theme { case light, dark, system } ``` L'élément crucial : `@State` fonctionne avec des types valeur (structs, enums, types primitifs). Pour les types référence (classes), d'autres outils sont nécessaires. ## Le macro @Observable expliqué Introduit avec iOS 17, `@Observable` transforme n'importe quelle classe en source de données réactive. Contrairement à l'ancien protocole `ObservableObject`, ce macro offre une granularité fine : seules les propriétés réellement lues par une vue déclenchent son re-render. ```swift // UserModel.swift import Observation // @Observable transforme la classe en source réactive @Observable class UserModel { var name: String = "" var email: String = "" var avatarURL: URL? var preferences = UserPreferences() // Les propriétés calculées fonctionnent aussi var isValid: Bool { !name.isEmpty && email.contains("@") } } struct UserPreferences { var newsletter: Bool = false var notifications: Bool = true var theme: Theme = .system } ``` La magie opère à la compilation : le macro génère automatiquement le code de tracking nécessaire pour chaque propriété. ### Observation granulaire en action La différence majeure avec l'ancien `ObservableObject` réside dans la granularité du tracking : ```swift // GranularObservation.swift @Observable class ProfileModel { var name: String = "" var bio: String = "" var followerCount: Int = 0 var posts: [Post] = [] } struct ProfileHeaderView: View { let model: ProfileModel var body: some View { VStack { // Cette vue ne re-render que si name ou bio changent Text(model.name) .font(.title) Text(model.bio) .foregroundStyle(.secondary) } } } struct FollowerCountView: View { let model: ProfileModel var body: some View { // Cette vue ne re-render que si followerCount change HStack { Image(systemName: "person.2") Text("\(model.followerCount) abonnés") } } } struct ProfileScreen: View { @State private var model = ProfileModel() var body: some View { VStack { // Chaque sous-vue track uniquement ses dépendances ProfileHeaderView(model: model) FollowerCountView(model: model) Button("Simuler nouveau follower") { // Ne re-render que FollowerCountView model.followerCount += 1 } } } } ``` > **Tracking automatique** > > SwiftUI analyse le `body` de chaque vue pour déterminer quelles propriétés sont lues. Seules ces propriétés déclenchent un re-render lors de leur modification. ## Comparaison directe @Observable vs @State Le choix entre ces deux outils dépend de plusieurs facteurs. Voici une comparaison structurée : ```swift // ComparisonExample.swift // Scénario 1 : État UI temporaire → @State struct ToggleExample: View { @State private var isExpanded = false // ✅ @State approprié var body: some View { VStack { Button(isExpanded ? "Réduire" : "Développer") { withAnimation { isExpanded.toggle() } } if isExpanded { Text("Contenu détaillé...") } } } } // Scénario 2 : Données métier partagées → @Observable @Observable class CartModel { // ✅ @Observable approprié var items: [CartItem] = [] var promoCode: String? var total: Decimal { items.reduce(0) { $0 + $1.price * Decimal($1.quantity) } } var itemCount: Int { items.reduce(0) { $0 + $1.quantity } } func addItem(_ item: CartItem) { if let index = items.firstIndex(where: { $0.id == item.id }) { items[index].quantity += 1 } else { items.append(item) } } func removeItem(_ item: CartItem) { items.removeAll { $0.id == item.id } } } struct CartItem: Identifiable, Equatable { let id: UUID let name: String let price: Decimal var quantity: Int } ``` ### Tableau récapitulatif des cas d'usage | Critère | @State | @Observable | |---------|--------|-------------| | Type de données | Types valeur (struct, enum) | Classes | | Scope | Local à une vue | Partageable entre vues | | Complexité | État simple | Logique métier complexe | | Cycle de vie | Géré par SwiftUI | Géré explicitement | | Re-render | Vue entière | Granulaire par propriété | ## Patterns d'utilisation avancés ### Combiner @State et @Observable Dans les applications réelles, ces outils coexistent harmonieusement. `@State` gère l'état UI local tandis que `@Observable` encapsule les données métier. ```swift // CombinedPatterns.swift @Observable class TodoListModel { var todos: [Todo] = [] var filter: TodoFilter = .all var filteredTodos: [Todo] { switch filter { case .all: return todos case .active: return todos.filter { !$0.isCompleted } case .completed: return todos.filter { $0.isCompleted } } } func addTodo(title: String) { let todo = Todo(id: UUID(), title: title, isCompleted: false) todos.append(todo) } func toggleTodo(_ todo: Todo) { guard let index = todos.firstIndex(where: { $0.id == todo.id }) else { return } todos[index].isCompleted.toggle() } } struct Todo: Identifiable, Equatable { let id: UUID var title: String var isCompleted: Bool } enum TodoFilter: CaseIterable { case all, active, completed } struct TodoListView: View { // Données métier via @Observable @State private var model = TodoListModel() // État UI local via @State @State private var newTodoTitle = "" @State private var isAddingTodo = false @State private var selectedTodo: Todo? var body: some View { NavigationStack { VStack { // Filtre avec Picker Picker("Filtre", selection: $model.filter) { ForEach(TodoFilter.allCases, id: \.self) { filter in Text(filter.label).tag(filter) } } .pickerStyle(.segmented) .padding() // Liste des todos List(model.filteredTodos, selection: $selectedTodo) { todo in TodoRowView(todo: todo) { model.toggleTodo(todo) } } } .navigationTitle("Tâches") .toolbar { Button { isAddingTodo = true } label: { Image(systemName: "plus") } } .sheet(isPresented: $isAddingTodo) { AddTodoSheet(model: model) } } } } struct TodoRowView: View { let todo: Todo let onToggle: () -> Void var body: some View { HStack { Image(systemName: todo.isCompleted ? "checkmark.circle.fill" : "circle") .foregroundStyle(todo.isCompleted ? .green : .secondary) .onTapGesture(perform: onToggle) Text(todo.title) .strikethrough(todo.isCompleted) } } } extension TodoFilter { var label: String { switch self { case .all: return "Toutes" case .active: return "Actives" case .completed: return "Terminées" } } } ``` ### @Observable avec injection de dépendances Pour les applications plus complexes, l'injection via l'environnement SwiftUI permet un découplage efficace : ```swift // DependencyInjection.swift @Observable class AuthenticationService { var currentUser: User? var isAuthenticated: Bool { currentUser != nil } func login(email: String, password: String) async throws { // Logique d'authentification currentUser = User(id: UUID(), email: email, name: "Utilisateur") } func logout() { currentUser = nil } } struct User: Identifiable, Equatable { let id: UUID let email: String let name: String } // Extension pour créer une clé d'environnement extension EnvironmentValues { @Entry var authService: AuthenticationService = AuthenticationService() } // Configuration dans l'App @main struct MyApp: App { @State private var authService = AuthenticationService() var body: some Scene { WindowGroup { ContentView() .environment(\.authService, authService) } } } // Utilisation dans les vues struct ProfileView: View { @Environment(\.authService) private var authService var body: some View { if let user = authService.currentUser { VStack { Text("Bonjour, \(user.name)") Button("Déconnexion") { authService.logout() } } } else { Text("Non connecté") } } } ``` ## Performance et optimisation ### Éviter les re-renders inutiles Même avec la granularité de `@Observable`, certains patterns peuvent dégrader les performances : ```swift // PerformanceOptimization.swift // ❌ Mauvais pattern : lecture de tout l'objet struct BadPatternView: View { let model: ProfileModel var body: some View { // Lit model.name ET model.posts même si seul name est affiché let _ = model.posts.count // Crée une dépendance inutile Text(model.name) } } // ✅ Bon pattern : lecture ciblée struct GoodPatternView: View { let model: ProfileModel var body: some View { // Track uniquement name Text(model.name) } } // ✅ Extraction en sous-vues pour isoler les dépendances struct OptimizedProfileView: View { let model: ProfileModel var body: some View { VStack { // Chaque sous-vue a ses propres dépendances ProfileNameView(model: model) ProfilePostsView(model: model) ProfileStatsView(model: model) } } } struct ProfileNameView: View { let model: ProfileModel var body: some View { Text(model.name) .font(.title) } } struct ProfilePostsView: View { let model: ProfileModel var body: some View { ForEach(model.posts) { post in PostRow(post: post) } } } struct ProfileStatsView: View { let model: ProfileModel var body: some View { HStack { StatBadge(value: model.followerCount, label: "Abonnés") StatBadge(value: model.posts.count, label: "Posts") } } } ``` > **Propriétés calculées coûteuses** > > Les propriétés calculées de `@Observable` sont réévaluées à chaque accès. Pour des calculs complexes, envisagez de mettre en cache le résultat dans une propriété stockée. ### Batch updates avec withObservationTracking Pour des scénarios avancés, `withObservationTracking` permet de détecter les changements sans créer de binding : ```swift // ObservationTracking.swift import Observation @Observable class DataSyncModel { var lastSyncDate: Date? var pendingChanges: Int = 0 var isSyncing: Bool = false } class SyncCoordinator { let model: DataSyncModel init(model: DataSyncModel) { self.model = model startObserving() } private func startObserving() { // Observe les changements sans UI withObservationTracking { // Accès qui crée les dépendances _ = model.pendingChanges _ = model.isSyncing } onChange: { // Appelé quand une propriété observée change Task { @MainActor in self.handleModelChange() } } } private func handleModelChange() { if model.pendingChanges > 0 && !model.isSyncing { // Déclenche la synchronisation Task { await syncChanges() } } // Ré-établit l'observation startObserving() } private func syncChanges() async { model.isSyncing = true // Logique de sync... model.isSyncing = false model.pendingChanges = 0 model.lastSyncDate = Date() } } ``` ## Migration depuis ObservableObject Pour les projets existants utilisant `ObservableObject`, la migration vers `@Observable` simplifie le code : ```swift // MigrationExample.swift // ❌ Ancien pattern avec ObservableObject class OldSettingsModel: ObservableObject { @Published var darkMode: Bool = false @Published var fontSize: CGFloat = 16 @Published var notifications: Bool = true } struct OldSettingsView: View { @StateObject private var settings = OldSettingsModel() // ou @ObservedObject si injecté var body: some View { Form { Toggle("Mode sombre", isOn: $settings.darkMode) Slider(value: $settings.fontSize, in: 12...24) Toggle("Notifications", isOn: $settings.notifications) } } } // ✅ Nouveau pattern avec @Observable @Observable class NewSettingsModel { var darkMode: Bool = false var fontSize: CGFloat = 16 var notifications: Bool = true } struct NewSettingsView: View { @State private var settings = NewSettingsModel() var body: some View { Form { Toggle("Mode sombre", isOn: $settings.darkMode) Slider(value: $settings.fontSize, in: 12...24) Toggle("Notifications", isOn: $settings.notifications) } } } ``` Les avantages de la migration : - Plus besoin de `@Published` sur chaque propriété - `@State` remplace `@StateObject` pour la création - Observation granulaire automatique - Code plus lisible et maintenable ## Règles de décision pratiques Voici un guide décisionnel pour choisir le bon outil : ```swift // DecisionGuide.swift /* RÈGLE 1 : État UI éphémère → @State - Animations, transitions - États de formulaires locaux - Sélections temporaires - Expansion/collapse de sections */ struct AnimatedCard: View { @State private var isFlipped = false // ✅ État UI local // ... } /* RÈGLE 2 : Données partagées entre vues → @Observable - Modèles de données métier - État d'authentification - Panier d'achat - Paramètres utilisateur */ @Observable class UserSession { // ✅ Partagé dans l'app var user: User? var preferences: Preferences // ... } /* RÈGLE 3 : Struct simple avec binding → @State - Configuration locale - Formulaires isolés */ struct FormData { var name: String = "" var email: String = "" } struct FormView: View { @State private var formData = FormData() // ✅ Struct avec @State // ... } /* RÈGLE 4 : Logique métier complexe → @Observable - Validations - Appels réseau - Transformations de données */ @Observable class OrderProcessor { // ✅ Logique complexe var items: [OrderItem] = [] var status: OrderStatus = .draft func validate() -> [ValidationError] { /* ... */ } func submit() async throws { /* ... */ } } ``` ## Conclusion Le choix entre `@Observable` et `@State` se résume à deux questions fondamentales : le type de données (valeur ou référence) et le scope de l'état (local ou partagé). `@State` excelle pour l'état UI simple et local, tandis que `@Observable` brille pour les modèles de données complexes nécessitant une observation granulaire. ### Checklist de décision - ✅ Utiliser `@State` pour les types valeur et l'état UI éphémère - ✅ Utiliser `@Observable` pour les classes avec données métier - ✅ Préférer `@Observable` quand l'état traverse plusieurs vues - ✅ Extraire en sous-vues pour optimiser les re-renders - ✅ Éviter de lire des propriétés non nécessaires dans le body - ✅ Migrer progressivement depuis `ObservableObject` - ✅ Utiliser l'environnement pour l'injection de dépendances - ✅ Tester les performances avec Instruments pour les cas complexes --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/fr/blog/ios/swiftui-observable-vs-state