2025 SwiftSwiftUI & UI FrameworksDeveloper Tools
WWDC25 · 36 min · Swift / SwiftUI & UI Frameworks / Developer Tools
Optimize SwiftUI performance with Instruments
Discover the new SwiftUI instrument. We’ll cover how SwiftUI updates views, how changes in your app’s data affect those updates, and how the new instrument helps you visualize those causes and effects. To get the most out of this session, we recommend being familiar with writing apps in SwiftUI.
Watch at developer.apple.com ↗Chapters
Code shown on screen · 8 snippets
LandmarkListItemView
import SwiftUI
import CoreLocation
/// A view that shows a single landmark in a list.
struct LandmarkListItemView: View {
(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))
}
} LocationFinder Class with Cached Distance Strings
import CoreLocation
/// A class the app uses to find the current location.
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()
}
} LandmarkListItemView with Favorite Button
import SwiftUI
import CoreLocation
/// A view that shows a single landmark in a list.
struct LandmarkListItemView: View {
(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()
}
}
} ModelData Class
/// A structure that defines a collection of landmarks.
class LandmarkCollection: Identifiable {
// ...
var landmarks: [Landmark] = []
// ...
}
/// A class the app uses to store and manage model data.
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)
}
}
// ...
} OnOffView
struct OnOffView: View {
private var isOn = true
var body: some View {
Text(isOn ? "On" : "Off")
}
} Favorites View Model Class
class ViewModel {
var isFavorite: Bool
init(isFavorite: Bool = false) {
self.isFavorite = isFavorite
}
} ModelData Class with New ViewModel
class ModelData {
// ...
var favoritesCollection: LandmarkCollection!
// ...
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
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
}
// ...
} Cause and effect: EnvironmentValues
struct View1: View {
(\.colorScheme)
private var colorScheme
var body: some View {
Text(colorScheme == .dark
? "Dark Mode"
: "Light Mode")
}
}
struct View2: View {
(\.counter) private var counter
var body: some View {
Text("\(counter)")
}
} Resources
Related sessions
-
33 min -
43 min -
22 min -
30 min -
27 min -
40 min