Modèles SwiftUI
Modèles SwiftUI modernes pour créer des interfaces utilisateur déclaratives et performantes sur les plateformes Apple. Couvre le framework Observation, la composition de vues, la navigation typée et l'optimisation des performances.
Quand Activer
- Construction de vues SwiftUI et gestion d'état (
@State,@Observable,@Binding) - Conception de flux de navigation avec
NavigationStack - Structuration de view models et flux de données
- Optimisation des performances de rendu pour les listes et les mises en page complexes
- Travailler avec les valeurs d'environnement et l'injection de dépendances dans SwiftUI
Gestion d'État
Sélection du Wrapper de Propriété
Choisissez le wrapper le plus simple qui correspond au besoin :
| Wrapper | Cas d'Utilisation |
|---|---|
@State | Types de valeur locaux à la vue (interrupteurs, champs de formulaire, présentation de feuille) |
@Binding | Référence bidirectionnelle vers @State du parent |
Classe @Observable + @State | Modèle possédé avec plusieurs propriétés |
Classe @Observable (sans wrapper) | Référence en lecture seule transmise depuis le parent |
@Bindable | Liaison bidirectionnelle vers une propriété @Observable |
@Environment | Dépendances partagées injectées via .environment() |
ViewModel avec @Observable
Utilisez @Observable (pas ObservableObject) — il suit les changements au niveau de la propriété, de sorte que SwiftUI ne réaffiche que les vues qui lisent la propriété modifiée :
@Observable
final class ItemListViewModel {
private(set) var items: [Item] = []
private(set) var isLoading = false
var searchText = ""
private let repository: any ItemRepository
init(repository: any ItemRepository = DefaultItemRepository()) {
self.repository = repository
}
func load() async {
isLoading = true
defer { isLoading = false }
items = (try? await repository.fetchAll()) ?? []
}
}
Vue Consommant le ViewModel
struct ItemListView: View {
@State private var viewModel: ItemListViewModel
init(viewModel: ItemListViewModel = ItemListViewModel()) {
_viewModel = State(initialValue: viewModel)
}
var body: some View {
List(viewModel.items) { item in
ItemRow(item: item)
}
.searchable(text: $viewModel.searchText)
.overlay { if viewModel.isLoading { ProgressView() } }
.task { await viewModel.load() }
}
}
Injection d'Environnement
Remplacez @EnvironmentObject par @Environment :
// Injection
ContentView()
.environment(authManager)
// Consommation
struct ProfileView: View {
@Environment(AuthManager.self) private var auth
var body: some View {
Text(auth.currentUser?.name ?? "Invité")
}
}
Composition de Vues
Extraire des Sous-vues pour Limiter l'Invalidation
Décomposez les vues en petites structures ciblées. Lorsque l'état change, seule la sous-vue qui lit cet état se réaffiche :
struct OrderView: View {
@State private var viewModel = OrderViewModel()
var body: some View {
VStack {
OrderHeader(title: viewModel.title)
OrderItemList(items: viewModel.items)
OrderTotal(total: viewModel.total)
}
}
}
ViewModifier pour un Style Réutilisable
struct CardModifier: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(.regularMaterial)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
extension View {
func cardStyle() -> some View {
modifier(CardModifier())
}
}
Navigation
NavigationStack Typée
Utilisez NavigationStack avec NavigationPath pour un routage programmatique et typé :
@Observable
final class Router {
var path = NavigationPath()
func navigate(to destination: Destination) {
path.append(destination)
}
func popToRoot() {
path = NavigationPath()
}
}
enum Destination: Hashable {
case detail(Item.ID)
case settings
case profile(User.ID)
}
struct RootView: View {
@State private var router = Router()
var body: some View {
NavigationStack(path: $router.path) {
HomeView()
.navigationDestination(for: Destination.self) { dest in
switch dest {
case .detail(let id): ItemDetailView(itemID: id)
case .settings: SettingsView()
case .profile(let id): ProfileView(userID: id)
}
}
}
.environment(router)
}
}
Performances
Utiliser des Conteneurs Paresseux pour les Grandes Collections
LazyVStack et LazyHStack créent les vues uniquement lorsqu'elles sont visibles :
ScrollView {
LazyVStack(spacing: 8) {
ForEach(items) { item in
ItemRow(item: item)
}
}
}
Identifiants Stables
Utilisez toujours des identifiants stables et uniques dans ForEach — évitez d'utiliser les indices de tableau :
// Utilisez la conformité Identifiable ou un id explicite
ForEach(items, id: \.stableID) { item in
ItemRow(item: item)
}
Éviter le Travail Coûteux dans body
- N'effectuez jamais d'E/S, d'appels réseau ou de calculs lourds à l'intérieur de
body - Utilisez
.task {}pour le travail asynchrone — il s'annule automatiquement lorsque la vue disparaît - Utilisez
.sensoryFeedback()et.geometryGroup()avec parcimonie dans les scroll views - Minimisez
.shadow(),.blur(), et.mask()dans les listes — ils déclenchent un rendu hors écran
Conformité Equatable
Pour les vues avec un body coûteux, conformez-vous à Equatable pour éviter les réaffichages inutiles :
struct ExpensiveChartView: View, Equatable {
let dataPoints: [DataPoint] // DataPoint doit être conforme à Equatable
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.dataPoints == rhs.dataPoints
}
var body: some View {
// Rendu de graphique complexe
}
}
Aperçus
Utilisez la macro #Preview avec des données mock en ligne pour une itération rapide :
#Preview("État vide") {
ItemListView(viewModel: ItemListViewModel(repository: EmptyMockRepository()))
}
#Preview(\