-
Otimize o desempenho do SwiftUI com o Instruments
Descubra o novo instrumento SwiftUI. Abordaremos como o SwiftUI atualiza as visualizações, como as alterações nos dados do seu app afetam essas atualizações e como o novo instrumento ajuda você a visualizar essas causas e efeitos.
Para aproveitar ao máximo esta sessão, recomendamos que você se familiarize com o desenvolvimento de apps em SwiftUI.Capítulos
- 0:00 - Introdução
- 2:19 - Descobrir o instrumento SwiftUI
- 4:20 - Diagnosticar e corrigir demoras nas atualizações do corpo da visualização
- 19:54 - Entender as causas e os efeitos das atualizações do SwiftUI
- 35:01 - Próximas etapas
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
Vídeos relacionados
WWDC25
WWDC23
WWDC22
WWDC21
Tech Talks
-
Buscar neste vídeo...
-
-
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 - Introdução
Saiba como otimizar o desempenho de apps SwiftUI com o novo SwiftUI e o modelo no Instruments 26. Use o Instruments para analisar o desempenho dos apps e identificar gargalos, como atualizações longas de corpo de visualizações e atualizações desnecessárias do SwiftUI, que podem causar travamentos, pausas em animações e rolagem atrasada. O app de exemplo apresentado, Landmarks, exibe pontos de referência globais e suas distâncias em relação à localização do usuário. Veja como usar o novo SwiftUI para melhorar a rolagem do app, diagnosticando e resolvendo problemas de desempenho no código SwiftUI.
- 2:19 - Descobrir o instrumento SwiftUI
O Instruments 26 apresenta um novo instrumento SwiftUI e modelo de profiling de apps em SwiftUI. Similar aos instrumentos Time Profiler e Hangs and Hitches, ele ajuda a identificar problemas de desempenho. A faixa Update Groups exibe o trabalho do SwiftUI. As três faixas destacam atualizações longas do corpo da visualização, atualizações longas dos representáveis e outras atualizações: laranja e vermelho com base na probabilidade de causarem travamentos. Para usar o novo SwiftUI, instale o Xcode 26 e atualize o sistema operacional do dispositivo para a versão mais recente.
- 4:20 - Diagnosticar e corrigir demoras nas atualizações do corpo da visualização
O exemplo usa o Xcode 26 e o Instruments 26 para analisar o desempenho do app Landmarks, que é escrito em SwiftUI. Comece abrindo o Instruments e selecionando o modelo SwiftUI para registrar o desempenho do app. Depois, interaja com o app no iPhone, rolando pela lista de pontos de referência, o que carrega visualizações adicionais. Após a gravação ser interrompida, o Instruments processa os dados, e você pode analisar a trilha do SwiftUI. Foque a trilha Long View Body Updates, para identificar visualizações, como 'LandmarkListItemView', que causam problemas de desempenho. Ao expandir a trilha SwiftUI e usar o instrumento Time Profiler, analise o uso da CPU durante as atualizações do corpo da visualização. Certas propriedades computadas, como formatadores usados para converter e exibir dados de distância, podem estar consumindo tempo excessivo. Otimize o tempo de execução do corpo da visualização no SwiftUI, pois eles são executados na thread principal. Atrasos podem fazer o app perder os prazos dos quadros, resultando em travamentos. Interrupções fazem com que as animações pareçam menos fluidas e podem impactar a experiência do usuário. Para resolver problemas de desempenho no projeto de exemplo, calcule e armazene em cache a string de distância com antecedência, em vez de realizar esses cálculos durante a atualização do corpo da visualização, garantindo um desempenho responsivo do app. No Xcode, há um processo de otimização na classe 'LocationFinder', que gerencia as atualizações de localização. O sistema calculava as strings de distância formatadas dentro do corpo da visualização de 'LandmarkListItemView', resultando em atualizações ineficientes. Para resolver isso, a lógica foi movida para a classe 'LocationFinder'. Aqui, o sistema cria e armazena os formatadores no inicializador para serem reutilizados, evitando a criação redundante. Um dicionário armazena em cache as strings de distância após o cálculo. A função 'updateDistances' é responsável por recalcular essas strings sempre que a localização mudar. Essa função utiliza os formatadores criados anteriormente para gerar a string de distância e armazená-la no cache. O framework CoreLocation chama o método 'locationManager(_:didUpdateLocations:)' no objeto 'CLLocationManagerDelegate' quando a localização do dispositivo é alterada. Ao chamar 'updateDistances' dentro desse método, o cache é mantido atualizado. As visualizações recuperam as strings de distância em cache, eliminando o recálculo durante as atualizações do corpo da visualização. A seguir, adicione um novo recurso: um botão de coração para favoritar pontos de referência. Quando alguém toca no botão, a função 'toggleFavorite' é chamada, atualizando a classe de dados do modelo para adicionar ou remover o ponto de referência da lista de favoritos. A visualização reflete essa mudança exibindo um ícone de coração preenchido ou vazio. Ao avaliar o desempenho do novo recurso no app usando o Instruments, a 'LandmarkListItemView' foi atualizada com mais frequência do que o esperado. Esse comportamento inesperado leva à investigação da lógica de atualização da visualização, evidenciando os desafios de depurar atualizações no SwiftUI em comparação com apps UIKit, em que a inspeção com breakpoints não é tão direta para frameworks declarativos.
- 19:54 - Entender as causas e os efeitos das atualizações do SwiftUI
No Xcode, depurar código imperativo, como em apps UIKit, é simples usando backtraces. No entanto, essa abordagem se torna menos eficaz com SwiftUI devido à sua natureza declarativa. O modelo de dados do SwiftUI, 'AttributeGraph', gerencia dependências entre visualizações, otimizando as atualizações. Quando uma visualização SwiftUI é declarada, está conforme o protocolo 'View' e define sua aparência e comportamento pela propriedade 'body'. Essa propriedade 'body' retorna outro valor 'View', e o SwiftUI gerencia o estado da visualização e as atualizações usando atributos. Alterações nas variáveis de estado acionam transações, marcando os atributos como desatualizados. O SwiftUI atualiza a hierarquia de visualizações durante o próximo quadro, percorrendo a cadeia de dependências para atualizar as partes necessárias. Para entender por que uma visualização SwiftUI foi atualizada, use o novo gráfico de Causa & Efeito do Instrument. Esse gráfico visualiza as relações entre as atualizações, mostrando a cadeia de causas desde interações do usuário, como gestos, até mudanças no estado e atualizações do corpo da visualização. Examinando esse gráfico, você pode identificar ineficiências, como atualizações desnecessárias, e otimizar o código de acordo. No app Landmarks, a classe 'ModelData' contém uma 'favoritesCollection', que armazena os pontos de referência favoritos em uma matriz. Cada 'LandmarkListItemView' verificava se um ponto de referência era favorito acessando 'favoritesCollection', criando uma dependência entre cada visualização de item e a matriz completa. Isso gerou ineficiência, pois, ao adicionar um favorito, o corpo de cada visualização de item era executado. Para resolver esse problema, a abordagem foi repensada. 'Observable' foi criado para cada ponto de referência, armazenando seu status de favorito. Cada 'LandmarkListItemView' tem seu próprio modelo de dados, eliminando a dependência da matriz completa de favoritos. Com essa alteração, o sistema atualiza o corpo da visualização necessário quando alguém altera o status de um favorito. Essa otimização melhora o desempenho, como demonstrado pela redução no número de atualizações do corpo da visualização observadas no gráfico de Causa & Efeito. O gráfico mostra como as atualizações no ambiente, como mudanças no esquema de cores, podem afetar as visualizações. Mesmo que o corpo de uma visualização seja executado devido a uma atualização no ambiente, ainda há um custo associado à verificação dessas atualizações. Evitar armazenar valores que mudam com frequência no ambiente.
- 35:01 - Próximas etapas
Para o novo instrumento SwiftUI no Instruments 26, recursos adicionais, vídeos e materiais relacionados à análise e melhoria de desempenho de apps estão na documentação do desenvolvedor.