-
Optimiser les performances de SwiftUI avec Instruments
Découvrez le nouvel instrument SwiftUI. Nous verrons comment les vues des mises à jour de SwiftUI, comment les modifications apportées aux données de votre application affectent ces mises à jour et comment le nouvel instrument vous aide à visualiser ces causes et effets.
Pour tirer le meilleur parti de cette séance, nous vous recommandons de vous familiariser avec les apps d'écriture dans SwiftUI.Chapitres
- 0:00 - Introduction et ordre du jour
- 2:19 - Découvrez l’instrument SwiftUI.
- 4:20 - Diagnostiquer et corriger les mises à jour longues de la vue du corps
- 19:54 - Comprendre les causes et les effets des mises à jour de SwiftUI
- 35:01 - Étapes suivantes
Ressources
- Performance and metrics
- Measuring your app’s power use with Power Profiler
- Understanding and improving SwiftUI performance
- Analyzing the performance of your visionOS app
- Improving app responsiveness
Vidéos connexes
WWDC25
WWDC23
WWDC22
WWDC21
Tech Talks
-
Rechercher dans cette vidéo…
-
-
8:47 - LandmarkListItemView
import SwiftUI import CoreLocation /// A view that shows a single landmark in a list. struct LandmarkListItemView: View { @Environment(ModelData.self) private var modelData let landmark: Landmark var body: some View { Image(landmark.thumbnailImageName) .resizable() .aspectRatio(contentMode: .fill) .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) .overlay { ... } .clipped() .cornerRadius(Constants.cornerRadius) .overlay(alignment: .bottom) { VStack(spacing: 6) { Text(landmark.name) .font(.title3).fontWeight(.semibold) .multilineTextAlignment(.center) .foregroundColor(.white) if let distance { Text(distance) .font(.callout) .foregroundStyle(.white.opacity(0.9)) .padding(.bottom) } } } .contextMenu { ... } } private var distance: String? { guard let currentLocation = modelData.locationFinder.currentLocation else { return nil } let distance = currentLocation.distance(from: landmark.clLocation) let numberFormatter = NumberFormatter() numberFormatter.numberStyle = .decimal numberFormatter.maximumFractionDigits = 0 let formatter = MeasurementFormatter() formatter.locale = Locale.current formatter.unitStyle = .medium formatter.unitOptions = .naturalScale formatter.numberFormatter = numberFormatter return formatter.string(from: Measurement(value: distance, unit: UnitLength.meters)) } } -
12:13 - LocationFinder Class with Cached Distance Strings
import CoreLocation /// A class the app uses to find the current location. @Observable class LocationFinder: NSObject { var currentLocation: CLLocation? private let currentLocationManager: CLLocationManager = CLLocationManager() private let formatter: MeasurementFormatter override init() { // Format the numeric distance let numberFormatter = NumberFormatter() numberFormatter.numberStyle = .decimal numberFormatter.maximumFractionDigits = 0 // Format the measurement based on the current locale let formatter = MeasurementFormatter() formatter.locale = Locale.current formatter.unitStyle = .medium formatter.unitOptions = .naturalScale formatter.numberFormatter = numberFormatter self.formatter = formatter super.init() currentLocationManager.desiredAccuracy = kCLLocationAccuracyKilometer currentLocationManager.delegate = self } // MARK: - Landmark Distance var landmarks: [Landmark] = [] { didSet { updateDistances() } } private var distanceCache: [Landmark.ID: String] = [:] private func updateDistances() { guard let currentLocation else { return } // Populate the cache with each formatted distance string self.distanceCache = landmarks.reduce(into: [:]) { result, landmark in let distance = self.formatter.string( from: Measurement( value: currentLocation.distance(from: landmark.clLocation), unit: UnitLength.meters ) ) result[landmark.id] = distance } } // Call this function from the view to access the cached value func distance(from landmark: Landmark) -> String? { distanceCache[landmark.id] } } extension LocationFinder: CLLocationManagerDelegate { func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { switch currentLocationManager.authorizationStatus { case .authorizedWhenInUse, .authorizedAlways: currentLocationManager.requestLocation() case .notDetermined: currentLocationManager.requestWhenInUseAuthorization() default: currentLocationManager.stopUpdatingLocation() } } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { print("Found a location.") currentLocation = locations.last // Update the distance strings when the location changes updateDistances() } func locationManager(_ manager: CLLocationManager, didFailWithError error: any Error) { print("Received an error while trying to find a location: \(error.localizedDescription).") currentLocationManager.stopUpdatingLocation() } } -
16:51 - LandmarkListItemView with Favorite Button
import SwiftUI import CoreLocation /// A view that shows a single landmark in a list. struct LandmarkListItemView: View { @Environment(ModelData.self) private var modelData let landmark: Landmark var body: some View { Image(landmark.thumbnailImageName) .resizable() .aspectRatio(contentMode: .fill) .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) .overlay { ... } .clipped() .cornerRadius(Constants.cornerRadius) .overlay(alignment: .bottom) { ... } .contextMenu { ... } .overlay(alignment: .topTrailing) { let isFavorite = modelData.isFavorite(landmark) Button { modelData.toggleFavorite(landmark) } label: { Label { Text(isFavorite ? "Remove Favorite" : "Add Favorite") } icon: { Image(systemName: "heart") .symbolVariant(isFavorite ? .fill : .none) .contentTransition(.symbolEffect) .font(.title) .foregroundStyle(.background) .shadow(color: .primary.opacity(0.25), radius: 2, x: 0, y: 0) } } .labelStyle(.iconOnly) .padding() } } } -
17:20 - ModelData Class
/// A structure that defines a collection of landmarks. @Observable class LandmarkCollection: Identifiable { // ... var landmarks: [Landmark] = [] // ... } /// A class the app uses to store and manage model data. @Observable @MainActor class ModelData { // ... var favoritesCollection: LandmarkCollection! // ... func isFavorite(_ landmark: Landmark) -> Bool { var isFavorite: Bool = false if favoritesCollection.landmarks.firstIndex(of: landmark) != nil { isFavorite = true } return isFavorite } func toggleFavorite(_ landmark: Landmark) { if isFavorite(landmark) { removeFavorite(landmark) } else { addFavorite(landmark) } } func addFavorite(_ landmark: Landmark) { favoritesCollection.landmarks.append(landmark) } func removeFavorite(_ landmark: Landmark) { if let landmarkIndex = favoritesCollection.landmarks.firstIndex(of: landmark) { favoritesCollection.landmarks.remove(at: landmarkIndex) } } // ... } -
20:50 - OnOffView
struct OnOffView: View { @State private var isOn = true var body: some View { Text(isOn ? "On" : "Off") } } -
29:21 - Favorites View Model Class
@Observable class ViewModel { var isFavorite: Bool init(isFavorite: Bool = false) { self.isFavorite = isFavorite } } -
29:21 - ModelData Class with New ViewModel
@Observable @MainActor class ModelData { // ... var favoritesCollection: LandmarkCollection! // ... @Observable class ViewModel { var isFavorite: Bool init(isFavorite: Bool = false) { self.isFavorite = isFavorite } } // Don't observe this property because we only need to react to changes // to each view model individually, rather than the whole dictionary @ObservationIgnored private var viewModels: [Landmark.ID: ViewModel] = [:] private func viewModel(for landmark: Landmark) -> ViewModel { // Create a new view model for a landmark on first access if viewModels[landmark.id] == nil { viewModels[landmark.id] = ViewModel() } return viewModels[landmark.id]! } func isFavorite(_ landmark: Landmark) -> Bool { // When a SwiftUI view, such as LandmarkListItemView, calls // `isFavorite` from its body, accessing `isFavorite` on the // view model here establishes a direct dependency between // the view and the view model viewModel(for: landmark).isFavorite } func toggleFavorite(_ landmark: Landmark) { if isFavorite(landmark) { removeFavorite(landmark) } else { addFavorite(landmark) } } func addFavorite(_ landmark: Landmark) { favoritesCollection.landmarks.append(landmark) viewModel(for: landmark).isFavorite = true } func removeFavorite(_ landmark: Landmark) { if let landmarkIndex = favoritesCollection.landmarks.firstIndex(of: landmark) { favoritesCollection.landmarks.remove(at: landmarkIndex) } viewModel(for: landmark).isFavorite = false } // ... } -
31:34 - Cause and effect: EnvironmentValues
struct View1: View { @Environment(\.colorScheme) private var colorScheme var body: some View { Text(colorScheme == .dark ? "Dark Mode" : "Light Mode") } } struct View2: View { @Environment(\.counter) private var counter var body: some View { Text("\(counter)") } }
-
-
- 0:00 - Introduction et ordre du jour
Apprenez à optimiser les performances de votre app SwiftUI avec le nouvel outil SwiftUI et le modèle intégré dans Instruments 26. Profitez-en pour profiler vos apps et détecter les ralentissements liés aux mises à jour longues ou inutiles, responsables de saccades, blocages et décalages. L’app exemple présentée, Landmarks, affiche des monuments mondiaux et leur distance par rapport à la position de l’utilisateur. Apprenez à utiliser le nouvel instrument SwiftUI pour fluidifier le défilement en identifiant et en corrigeant les problèmes de performance dans le code SwiftUI.
- 2:19 - Découvrez l’instrument SwiftUI.
Instruments 26 introduit un nouvel instrument et modèle SwiftUI pour profiler les apps SwiftUI. Comme les instruments Time Profiler et Hangs and Hitches, il aide à identifier les problèmes de performance. La piste Update Groups affiche le travail SwiftUI. Les trois autres pistes signalent les longues mises à jour (en orange ou rouge selon leur probabilité de provoquer des saccades ou des blocages). Pour utiliser le nouvel instrument SwiftUI, installez Xcode 26 et mettez à jour le SE de votre appareil.
- 4:20 - Diagnostiquer et corriger les mises à jour longues de la vue du corps
L’exemple utilise Xcode 26 et Instruments 26 pour profiler l’app Landmarks, développée en SwiftUI. Commencez par lancer Instruments et sélectionnez le modèle SwiftUI pour enregistrer les performances de l’app. Interagissez ensuite avec l’app sur iPhone en faisant défiler une liste des monuments, ce qui charge des vues supplémentaires. Après l’arrêt de l’enregistrement, Instruments traite les données, et vous pouvez analyser la piste SwiftUI. Analysez la piste Long View Body Updates pour repérer les vues, comme LandmarkListItemView, causant des problèmes de performance. En développant la piste SwiftUI et en utilisant l’instrument Time Profiler, vous pouvez approfondir l’analyse de l’utilisation CPU lors des mises à jour de vues. Vous pouvez constater que certaines propriétés calculées (notamment les formateurs pour convertir et afficher les distances) consomment trop de temps. Il est important d’optimiser le temps d’exécution du corps de vue dans SwiftUI, car celui-ci s’exécute sur le thread principal et tout retard peut faire manquer des images, ce qui provoque des saccades. Les saccades rendent les animations moins fluides et peuvent nuire à l’expérience utilisateur. Pour résoudre ces problèmes de performance dans l’exemple, calculez et mettez en cache la chaîne de distance à l’avance, au lieu de faire ces calculs lors de la mise à jour du corps de la vue, afin d’assurer une app plus fluide et réactive. Dans Xcode, un processus d’optimisation est mis en place dans la classe LocationFinder, qui gère les mises à jour de position. Auparavant, les chaînes de distance étaient calculées dans le corps de vue de LandmarkListItemView, ce qui rendait les mises à jour inefficaces. Pour y remédier, la logique a été déplacée dans la classe LocationFinder. Le système crée et stocke les formateurs dans l’initialiseur pour les réutiliser, évitant ainsi des créations redondantes. Un dictionnaire met en cache les chaînes de distance après le calcul. La fonction updateDistances est chargée de recalculer ces chaînes à chaque changement de position. Cette fonction utilise les formateurs créés pour générer la chaîne de distance et l’enregistrer dans le cache. Le framework CoreLocation appelle la méthode locationManager(_:didUpdateLocations:) sur son objet CLLocationManagerDelegate lorsque la position de l’appareil change. En appelant updateDistances dans cette méthode, le cache reste à jour. Les vues accèdent aux chaînes de distance mises en cache, évitant ainsi de les recalculer lors des mises à jour du corps de la vue. Ajoutez ensuite une nouvelle fonctionnalité : un bouton cœur pour vos monuments préférés. Lorsque quelqu’un appuie sur le bouton, la fonction toggleFavorite est appelée et met à jour le modèle pour ajouter ou retirer le monument de la liste des favoris. La vue reflète alors ce changement en affichant une icône de cœur pleine ou vide. En analysant la nouvelle fonctionnalité de favoris dans Instruments, vous constaterez peut-être que LandmarkListItemView se met à jour plus souvent que prévu. Ce comportement inattendu pousse à examiner la logique de mise à jour de la vue, soulignant les difficultés à déboguer les mises à jour dans les apps SwiftUI, contrairement aux apps UIKit, où l’inspection par points d’arrêt est plus directe qu’avec un framework déclaratif.
- 19:54 - Comprendre les causes et les effets des mises à jour de SwiftUI
Dans Xcode, le débogage du code impératif, comme dans les apps UIKit, est simple grâce aux backtraces. Mais, cette approche est moins efficace avec SwiftUI en raison de sa nature déclarative. Le modèle de données de SwiftUI (AttributeGraph), gère les dépendances entre les vues pour optimiser les mises à jour. Lorsqu’une vue SwiftUI est déclarée, elle adopte le protocole View et définit son apparence et son comportement via la propriété body. La propriété body retourne une autre valeur View, et SwiftUI gère en interne l’état de la vue et ses mises à jour à l’aide d’attributs. Les changements d’état déclenchent des transactions marquant certains attributs comme obsolètes. SwiftUI met à jour la hiérarchie des vues lors de l’image suivante, en ne rafraîchissant que les vues nécessaires grâce à la chaîne de dépendances. Pour savoir pourquoi une vue SwiftUI s’est mise à jour, utilisez le nouveau graphe de causes et effets de l’instrument SwiftUI. Il visualise les liens entre les mises à jour, en montrant la chaîne des causes allant des interactions utilisateur, comme les gestes, jusqu’aux changements d’état et aux mises à jour du corps de vue. En examinant ce graphe, vous pouvez repérer des inefficacités, comme des mises à jour inutiles, et optimiser votre code. Dans l’app Landmarks, la classe ModelData contient une propriété favoritesCollection qui stocke les monuments ajoutés aux favoris dans un tableau. Au départ, chaque LandmarkListItemView vérifiait si un monument était en favori en accédant à l’ensemble du tableau favoritesCollection, créant ainsi une dépendance entre chaque vue et tout le tableau. Cela entraînait de mauvaises performances, car l’ajout d’un favori relançait le corps de chaque vue. Pour résoudre ce problème, l’approche a été repensée. Chaque monument dispose désormais d’un modèle Observable qui contient son statut de favori. Chaque LandmarkListItemView a désormais son propre modèle, sans dépendre du tableau des favoris. Grâce à ce changement, seule la vue concernée est mise à jour lorsqu’un favori est modifié. Cette optimisation améliore nettement les performances, comme le montre la baisse des mises à jour de vues dans le graphe de causes et effets. Ce graphe montre aussi comment des changements comme le thème de couleurs peuvent impacter les vues. Même si le corps d’une vue n’a pas besoin de s’exécuter après une mise à jour de l’environnement, vérifier ces changements a un coût : mieux vaut éviter d’y stocker des valeurs qui changent souvent.
- 35:01 - Étapes suivantes
Le nouvel instrument SwiftUI d’Instruments 26 est accompagné de vidéos, de fonctions avancées et de ressources sur l’optimisation des performances dans la documentation développeur.