-
Optimiza el rendimiento de SwiftUI con instrumentos
Descubre la nueva herramienta SwiftUI. Hablaremos sobre cómo SwiftUI actualiza las vistas, cómo los cambios en los datos de tu app afectan esas actualizaciones y cómo el nuevo instrumento te ayuda a visualizar esas causas y efectos.
Para aprovechar al máximo esta sesión, recomendamos familiarizarse con el desarrollo de apps en SwiftUI.Capítulos
- 0:00 - Introducción y agenda
- 2:19 - Descubre el instrumento SwiftUI.
- 4:20 - Diagnostica y corrige actualizaciones extensas del cuerpo de la vista
- 19:54 - Comprende las causas y los efectos de las actualizaciones de SwiftUI
- 35:01 - Próximos pasos
Recursos
- 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
Videos relacionados
WWDC25
WWDC23
WWDC22
WWDC21
Tech Talks
-
Buscar este video…
-
-
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 - Introducción y agenda
Aprende a optimizar el rendimiento de las apps de SwiftUI con el nuevo instrumento y plantilla de SwiftUI en Instruments 26. Puedes usar Instruments para crear perfiles en tus apps e identificar cuellos de botella, como actualizaciones extensas del cuerpo de la vista y actualizaciones innecesarias de SwiftUI, que pueden causar fallas, interrupciones, animaciones y transiciones pausadas, y un desplazamiento lento. La app de ejemplo que se presenta, llamada Landmarks, muestra puntos de referencia de todo el mundo y sus distancias desde la ubicación del usuario. Averigua cómo usar la nueva herramienta de SwiftUI para mejorar la fluidez de desplazamiento de tus apps mediante el diagnóstico y la resolución de problemas de rendimiento en el código SwiftUI.
- 2:19 - Descubre el instrumento SwiftUI.
Instruments 26 presenta un nuevo instrumento y plantilla de SwiftUI para crear perfiles de apps de SwiftUI. Similar a los instrumentos Time Profiler y Hangs and Hitches, ayuda a identificar problemas de rendimiento. La ruta Update Groups muestra el trabajo de SwiftUI. Las otras tres rutas resaltan actualizaciones extensas del cuerpo de la vista, actualizaciones representables extensas y otras actualizaciones de color naranja y rojo según su probabilidad de causar fallas o interrupciones. Para usar la nueva herramienta de SwiftUI, instala Xcode 26 y actualiza el sistema operativo de tu dispositivo para obtener la versión más reciente.
- 4:20 - Diagnostica y corrige actualizaciones extensas del cuerpo de la vista
En el ejemplo se usa Xcode 26 e Instruments 26 para crear perfiles de la app Landmarks, que se escribió en SwiftUI. Primero, abre Instruments y selecciona la plantilla de SwiftUI para registrar el rendimiento de la app. Luego, interactúa con la app en el iPhone y desplázate por una lista de puntos de referencia, que carga vistas adicionales. Una vez que se detiene la grabación, Instruments procesa los datos y luego puedes analizar el seguimiento de SwiftUI. Concéntrate en la ruta Long View Body Updates, donde puedes identificar vistas específicas, como LandmarkListItemView, que causan problemas de rendimiento. Al ampliar el seguimiento de SwiftUI y usar la herramienta Time Profiler, puedes profundizar en el uso del CPU durante las actualizaciones del cuerpo de la vista. Descubrirás que ciertas propiedades calculadas, particularmente los formateadores que se usan para convertir y mostrar datos de distancia, consumen demasiado tiempo. Ten en cuenta la importancia de optimizar el tiempo de ejecución del cuerpo de la vista en SwiftUI, en específico los que se ejecutan en el hilo principal. Cualquier demora puede provocar que la app no cumpla con los plazos de los cuadros, lo que generará fallas. Estas hacen que las animaciones parezcan menos fluidas y pueden afectar negativamente la experiencia general del usuario. Para solucionar estos problemas de rendimiento en el proyecto de ejemplo, calcula y almacena en caché la cadena de distancia con antelación, en lugar de realizar estos cálculos durante la actualización del cuerpo de la vista, para garantizar un rendimiento más fluido y con mayor capacidad de respuesta de la app. En Xcode, hay un proceso de optimización en la clase LocationFinder que gestiona las actualizaciones de ubicación. Anteriormente, el sistema calculaba cadenas de distancia formateadas dentro del cuerpo de la vista de LandmarkListItemView, lo que generaba actualizaciones ineficientes. Para solucionar esto, el código mueve esta lógica a la clase LocationFinder. Aquí, el sistema crea y almacena formateadores en el inicializador para reusarlos, evitando la creación redundante. Un diccionario almacena en caché las cadenas de distancia después del cálculo. La función updateDistances es responsable de volver a calcular estas cadenas cuando hay un cambio de ubicación. Esta usa los formateadores que se crearon antes para generar la cadena de distancia y almacenarla en caché. La estructura CoreLocation invoca al método locationManager(_:didUpdateLocations:) en su objeto CLLocationManagerDelegate cuando cambia la ubicación del dispositivo. Al invocar updateDistances dentro de este método, la memoria caché se mantiene actualizada. Luego, las vistas recuperan las cadenas de distancia almacenadas en caché, lo que elimina la necesidad de realizar nuevos cálculos durante las actualizaciones del cuerpo de la vista. Luego, puedes agregar una nueva funcionalidad: un botón de corazón para marcar puntos de referencia favoritos. Cuando una persona toca el botón, se invoca la función toggleFavorite, que actualiza la clase de datos del modelo para agregar o eliminar lugares de interés de la lista de favoritos. La vista luego refleja este cambio mostrando el ícono de corazón lleno o vacío. Al crear un perfil de la nueva funcionalidad de favoritos de la app en Instruments, es posible que notes que LandmarkListItemView se actualiza con más frecuencia de lo esperado. Este comportamiento inesperado impulsa una investigación sobre la lógica de actualización de la vista, lo que resalta los retos en la depuración de actualizaciones de vista en las apps de SwiftUI en comparación con las apps de UIKit, donde la inspección tradicional basada en puntos de interrupción puede no ser tan sencilla para las estructuras declarativas.
- 19:54 - Comprende las causas y los efectos de las actualizaciones de SwiftUI
En Xcode, la depuración de código imperativo, como en las apps de UIKit, es sencilla mediante el uso de seguimientos inversos. Sin embargo, este enfoque se vuelve menos efectivo con SwiftUI debido a su naturaleza declarativa. El modelo de datos de SwiftUI, AttributeGraph, administra las dependencias entre vistas, optimizando las actualizaciones. Cuando se declara una vista de SwiftUI, se ajusta al protocolo View y se define su apariencia y comportamiento a través de la propiedad body. Esta arroja otro valor View, y SwiftUI gestiona internamente el estado de la vista y las actualizaciones mediante atributos. Los cambios en las variables de estado desencadenan transacciones y marcan los atributos relevantes como obsoletos. Luego, SwiftUI actualiza de manera eficiente la jerarquía de vistas durante el siguiente cuadro, recorriendo la cadena de dependencia para actualizar solo las partes necesarias. Para comprender por qué se actualizó una vista de SwiftUI, puedes usar el nuevo gráfico de causa y efecto del instrumento de SwiftUI. Este gráfico visualiza las relaciones entre las actualizaciones y muestra la cadena de causas, que va desde las interacciones del usuario, como los gestos, hasta los cambios de estado y, en última instancia, las actualizaciones del cuerpo de la vista. Al examinar este gráfico, puedes identificar ineficiencias, como actualizaciones innecesarias, y optimizar su código en consecuencia. En la app Landmarks, la clase ModelData contiene la propiedad favoritesCollection, que almacena los puntos de referencia favoritos en una matriz. Inicialmente, cada LandmarkListItemView verificaba si un punto de referencia era un favorito accediendo a toda la matriz favoritesCollection, creando una dependencia entre cada vista de elemento y toda la matriz. Esto generó un rendimiento ineficiente ya que, cada vez que se agregaba un favorito, se ejecutaba el cuerpo de todas las vistas del elemento. Para abordar este problema, se replanteó el enfoque. Se creó un modelo de datos Observable para cada lugar de interés, almacenando directamente su estado de favorito. Cada LandmarkListItemView ahora tiene su propio modelo de datos, lo que elimina la dependencia de la matriz completa de favoritos. Al implementar este cambio, el sistema solo actualiza el cuerpo de vista necesario cuando el usuario activa o desactiva un favorito. Esta optimización mejora significativamente el rendimiento, como lo demuestra la cantidad reducida de actualizaciones del cuerpo de la vista que se puede observar en el gráfico de causa y efecto. El gráfico también muestra cómo las actualizaciones del entorno, como los cambios en el esquema de colores, pueden afectar las vistas. Incluso si no es necesario ejecutar el cuerpo de una vista debido a una actualización del entorno, aún existe un costo asociado a la verificación de estas actualizaciones, por lo que es importante evitar almacenar valores que cambian con frecuencia en el entorno.
- 35:01 - Próximos pasos
Para el nuevo instrumento de SwiftUI en Instruments 26, hay funcionalidades adicionales, videos y recursos relacionados sobre el análisis y la mejora del rendimiento de las apps disponibles en la documentación para desarrolladores.