Performance SwiftUI: Ottimizzazione di LazyVStack e Liste Complesse

Tecniche di ottimizzazione per LazyVStack e liste SwiftUI. Ridurre il consumo di memoria, migliorare le performance di scroll ed evitare errori comuni.

Ottimizzazione delle performance di SwiftUI LazyVStack e liste complesse

Le liste rappresentano uno dei componenti più utilizzati nelle applicazioni iOS. LazyVStack e List di SwiftUI offrono soluzioni performanti per visualizzare collezioni di dati, ma un utilizzo errato può degradare rapidamente l'esperienza utente. Comprendere il funzionamento interno di questi componenti aiuta a evitare errori frequenti e a costruire interfacce fluide.

Cosa copre questo articolo

Questo articolo presenta le tecniche essenziali di ottimizzazione per le liste SwiftUI: lazy loading, riciclo delle view, gestione degli identificatori e pattern avanzati per grandi dataset. Aggiornato per iOS 27 con le nuove API di riordino e swipe actions.

Comprendere il Lazy Loading in SwiftUI

Il lazy loading istanzia le view solo quando diventano visibili sullo schermo. A differenza di VStack che crea immediatamente tutte le view figlie, LazyVStack rimanda questa creazione, riducendo drasticamente il consumo di memoria e il tempo di rendering iniziale.

LazyVStackComparison.swiftswift
import SwiftUI

// ❌ Problem: VStack instantiates all 10,000 views immediately
struct NonLazyListView: View {
    let items = (1...10000).map { "Item (/bin/bash)" }

    var body: some View {
        ScrollView {
            VStack {
                ForEach(items, id: .self) { item in
                    // Each view is created at launch
                    ExpensiveRowView(title: item)
                }
            }
        }
    }
}

// ✅ Solution: LazyVStack creates views on demand
struct LazyListView: View {
    let items = (1...10000).map { "Item (/bin/bash)" }

    var body: some View {
        ScrollView {
            LazyVStack {
                ForEach(items, id: .self) { item in
                    // Only visible views are created
                    ExpensiveRowView(title: item)
                }
            }
        }
    }
}

La differenza di performance diventa significativa con appena qualche centinaio di elementi. Con 10.000 item, VStack può richiedere diversi secondi all'avvio mentre LazyVStack rimane istantaneo.

Misurare l'Impatto del Lazy Loading

Instruments permette di misurare con precisione l'utilizzo di memoria e CPU. Ecco una view di test che illustra la differenza:

PerformanceMeasurement.swiftswift
struct ExpensiveRowView: View {
    let title: String

    // Simulating expensive initialization
    init(title: String) {
        self.title = title
        // Log to visualize when the view is created
        print("Creating row: (title)")
    }

    var body: some View {
        HStack {
            // Image with processing
            Circle()
                .fill(
                    LinearGradient(
                        colors: [.blue, .purple],
                        startPoint: .topLeading,
                        endPoint: .bottomTrailing
                    )
                )
                .frame(width: 50, height: 50)

            VStack(alignment: .leading) {
                Text(title)
                    .font(.headline)
                Text("Subtitle with computation")
                    .font(.caption)
                    .foregroundStyle(.secondary)
            }

            Spacer()
        }
        .padding()
    }
}

Eseguendo con VStack, tutti i 10.000 log appaiono immediatamente. Con LazyVStack, solo gli elementi visibili (circa 15-20 a seconda della dimensione dello schermo) vengono loggati inizialmente, con altri che appaiono durante lo scroll.

Comportamento di retention

LazyVStack mantiene in memoria le view create dopo che appaiono. A differenza di List che ricicla attivamente le celle, le view in un LazyVStack persistono fino a quando il componente padre non viene distrutto.

L'Importanza degli Identificatori Stabili

Gli identificatori costituiscono il meccanismo centrale degli aggiornamenti delle liste SwiftUI. Un identificatore instabile causa ricreazioni inutili di view e può generare bug visivi come animazioni errate o perdita della posizione di scroll.

StableIdentifiers.swiftswift
// ❌ Problem: using index as identifier
struct UnstableIdentifierView: View {
    @State private var items = ["A", "B", "C", "D"]

    var body: some View {
        List {
            // Index changes if an element is deleted
            ForEach(items.indices, id: .self) { index in
                Text(items[index])
            }
        }
    }
}

// ❌ Problem: using UUID() in ForEach
struct RegeneratedIdentifierView: View {
    let items = ["A", "B", "C", "D"]

    var body: some View {
        List {
            // UUID() generates a new ID on each render
            ForEach(items, id: .self) { item in
                // Subtle issue if items contain duplicates
                Text(item)
            }
        }
    }
}

// ✅ Solution: model with stable identifier
struct Item: Identifiable {
    let id: UUID  // Created once
    var name: String

    init(name: String) {
        self.id = UUID()
        self.name = name
    }
}

struct StableIdentifierView: View {
    @State private var items = [
        Item(name: "A"),
        Item(name: "B"),
        Item(name: "C"),
        Item(name: "D")
    ]

    var body: some View {
        List {
            // id is stable for the item's lifetime
            ForEach(items) { item in
                Text(item.name)
            }
        }
    }
}

Utilizzare un identificatore unico e persistente garantisce che SwiftUI possa differenziare correttamente gli elementi durante aggiornamenti, animazioni e confronti.

Ottimizzazione delle Celle con Equatable

SwiftUI confronta le view per determinare se è necessario un nuovo rendering. Per default, questo confronto utilizza la reflection, che può essere costosa. Implementare Equatable permette un confronto ottimizzato ed esplicito.

EquatableOptimization.swiftswift
// Data model
struct Contact: Identifiable, Equatable {
    let id: UUID
    var name: String
    var email: String
    var avatarURL: URL?
    var lastActivity: Date

    // Custom comparison: ignore lastActivity
    // if other properties are identical
    static func == (lhs: Contact, rhs: Contact) -> Bool {
        lhs.id == rhs.id &&
        lhs.name == rhs.name &&
        lhs.email == rhs.email &&
        lhs.avatarURL == rhs.avatarURL
        // lastActivity intentionally excluded
    }
}

// Optimized cell view
struct ContactRow: View, Equatable {
    let contact: Contact

    // Explicit comparison to avoid unnecessary re-renders
    static func == (lhs: ContactRow, rhs: ContactRow) -> Bool {
        lhs.contact == rhs.contact
    }

    var body: some View {
        HStack(spacing: 12) {
            // Async avatar
            AsyncImage(url: contact.avatarURL) { phase in
                switch phase {
                case .success(let image):
                    image
                        .resizable()
                        .aspectRatio(contentMode: .fill)
                case .failure:
                    Image(systemName: "person.circle.fill")
                        .foregroundStyle(.gray)
                default:
                    ProgressView()
                }
            }
            .frame(width: 44, height: 44)
            .clipShape(Circle())

            // Contact information
            VStack(alignment: .leading, spacing: 2) {
                Text(contact.name)
                    .font(.body.weight(.medium))

                Text(contact.email)
                    .font(.caption)
                    .foregroundStyle(.secondary)
            }

            Spacer()
        }
        .padding(.vertical, 4)
    }
}

// List using EquatableView
struct ContactListView: View {
    let contacts: [Contact]

    var body: some View {
        List {
            ForEach(contacts) { contact in
                // EquatableView prevents re-renders if contact unchanged
                EquatableView(content: ContactRow(contact: contact))
            }
        }
    }
}

Questa ottimizzazione riduce significativamente il carico CPU durante lo scroll veloce, in particolare con celle che contengono calcoli o immagini.

Pronto a superare i tuoi colloqui su iOS?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

Gestione del Caricamento Asincrono delle Immagini

Le immagini diventano spesso il collo di bottiglia delle performance nelle liste. iOS 27 ha introdotto il caching HTTP standard per AsyncImage di default, eliminando la necessità di caching manuale in molti scenari. Per applicazioni che supportano iOS 26 e precedenti, o che richiedono un controllo fine sul comportamento di caching, un'implementazione personalizzata rimane utile.

ImageLoadingOptimization.swiftswift
import SwiftUI

// Singleton image cache (for iOS 26 or custom caching needs)
actor ImageCache {
    static let shared = ImageCache()

    private var cache = NSCache<NSString, UIImage>()

    private init() {
        // Memory limit: 50 MB
        cache.totalCostLimit = 50 * 1024 * 1024
    }

    func image(for url: URL) -> UIImage? {
        cache.object(forKey: url.absoluteString as NSString)
    }

    func setImage(_ image: UIImage, for url: URL) {
        // Cost estimation: image bytes
        let cost = Int(image.size.width * image.size.height * 4)
        cache.setObject(image, forKey: url.absoluteString as NSString, cost: cost)
    }
}

// Optimized image view with caching
struct CachedAsyncImage: View {
    let url: URL?
    let size: CGSize

    @State private var image: UIImage?
    @State private var isLoading = false

    var body: some View {
        Group {
            if let image {
                Image(uiImage: image)
                    .resizable()
                    .aspectRatio(contentMode: .fill)
            } else if isLoading {
                Rectangle()
                    .fill(Color.gray.opacity(0.2))
                    .overlay(ProgressView())
            } else {
                Rectangle()
                    .fill(Color.gray.opacity(0.2))
            }
        }
        .frame(width: size.width, height: size.height)
        .clipped()
        .task(id: url) {
            await loadImage()
        }
    }

    private func loadImage() async {
        guard let url else { return }

        // Check cache
        if let cached = await ImageCache.shared.image(for: url) {
            self.image = cached
            return
        }

        isLoading = true
        defer { isLoading = false }

        // Download and resize
        do {
            let (data, _) = try await URLSession.shared.data(from: url)

            // Resize to save memory
            if let original = UIImage(data: data),
               let resized = await resizeImage(original, to: size) {
                await ImageCache.shared.setImage(resized, for: url)
                self.image = resized
            }
        } catch {
            // Handle error silently
        }
    }

    private func resizeImage(_ image: UIImage, to size: CGSize) async -> UIImage? {
        // Use screen scale
        let scale = await UIScreen.main.scale
        let targetSize = CGSize(
            width: size.width * scale,
            height: size.height * scale
        )

        return await withCheckedContinuation { continuation in
            DispatchQueue.global(qos: .userInitiated).async {
                let renderer = UIGraphicsImageRenderer(size: targetSize)
                let resized = renderer.image { _ in
                    image.draw(in: CGRect(origin: .zero, size: targetSize))
                }
                continuation.resume(returning: resized)
            }
        }
    }
}

Per iOS 27 e successivi, AsyncImage standard rispetta automaticamente gli header di cache HTTP. Una URLCache personalizzata può essere applicata tramite il modifier asyncImageURLSession(_:) per requisiti specifici.

Prefetching Intelligente per l'Anticipazione

Per liste molto lunghe, il prefetching carica le immagini prima che diventino visibili:

ImagePrefetching.swiftswift
// Prefetching coordinator
@Observable
final class ImagePrefetcher {
    private var prefetchTasks: [URL: Task<Void, Never>] = [:]
    private let prefetchDistance = 10  // Number of items ahead

    func prefetchImages(for items: [Contact], visibleRange: Range<Int>) {
        // Calculate prefetch range
        let prefetchStart = max(0, visibleRange.lowerBound - prefetchDistance)
        let prefetchEnd = min(items.count, visibleRange.upperBound + prefetchDistance)

        // Launch prefetch for items in range
        for index in prefetchStart..<prefetchEnd {
            guard let url = items[index].avatarURL else { continue }

            // Avoid duplicates
            guard prefetchTasks[url] == nil else { continue }

            prefetchTasks[url] = Task {
                // Check if already cached
                if await ImageCache.shared.image(for: url) != nil {
                    return
                }

                // Prefetch
                do {
                    let (data, _) = try await URLSession.shared.data(from: url)
                    if let image = UIImage(data: data) {
                        await ImageCache.shared.setImage(image, for: url)
                    }
                } catch {
                    // Ignore prefetch errors
                }
            }
        }

        // Cancel out-of-range prefetches
        cancelOutOfRangePrefetches(validRange: prefetchStart..<prefetchEnd, items: items)
    }

    private func cancelOutOfRangePrefetches(validRange: Range<Int>, items: [Contact]) {
        let validURLs = Set(
            items[validRange].compactMap { $0.avatarURL }
        )

        for (url, task) in prefetchTasks {
            if !validURLs.contains(url) {
                task.cancel()
                prefetchTasks.removeValue(forKey: url)
            }
        }
    }
}

Scegliere tra List e LazyVStack

iOS 27 ha ridotto significativamente il divario tra List e LazyVStack. In precedenza, List era l'unica opzione per swipe actions native e drag-to-reorder. Le nuove API .swipeActionsContainer() e .reorderable() portano queste funzionalità in qualsiasi container.

ListVsLazyVStack.swiftswift
// ✅ List: automatic cell recycling, built-in styles
// Best for: standard contacts, settings, data tables
struct ContactsWithSwipeActions: View {
    @State private var contacts: [Contact] = []

    var body: some View {
        List {
            ForEach(contacts) { contact in
                ContactRow(contact: contact)
                    .swipeActions(edge: .trailing) {
                        Button(role: .destructive) {
                            deleteContact(contact)
                        } label: {
                            Label("Delete", systemImage: "trash")
                        }
                    }
                    .swipeActions(edge: .leading) {
                        Button {
                            favoriteContact(contact)
                        } label: {
                            Label("Favorite", systemImage: "star")
                        }
                        .tint(.yellow)
                    }
            }
        }
        .listStyle(.plain)
    }

    private func deleteContact(_ contact: Contact) {
        contacts.removeAll { $0.id == contact.id }
    }

    private func favoriteContact(_ contact: Contact) {
        // Favorite logic
    }
}

// ✅ LazyVStack with iOS 27 swipe actions
// Best for: custom layouts, cards, feeds
struct CustomFeedWithSwipeActions: View {
    @State private var posts: [Post] = []

    var body: some View {
        ScrollView {
            LazyVStack(spacing: 16) {
                ForEach(posts) { post in
                    PostCard(post: post)
                        .swipeActions(edge: .trailing) {
                            Button(role: .destructive) {
                                deletePost(post)
                            } label: {
                                Label("Delete", systemImage: "trash")
                            }
                        }
                }
            }
            .padding(.horizontal)
            .swipeActionsContainer()
        }
    }

    private func deletePost(_ post: Post) {
        posts.removeAll { $0.id == post.id }
    }
}

La scelta ora dipende dai requisiti visivi piuttosto che dalla disponibilità delle funzionalità:

RequisitoContainer Consigliato
Riciclo celle per efficienza memoriaList
Spaziatura e padding personalizzatiLazyVStack
Separatori e inset integratiList
Layout a card o non standardLazyVStack
Header sezione con comportamento stickyEntrambi (usare pinnedViews)
Swipe actionsEntrambi (iOS 27+)
Drag-to-reorderEntrambi (iOS 27+)
Riciclo delle celle

List ricicla attivamente le celle, il che può causare problemi con lo state locale (@State). I valori @State nelle celle di List possono essere riutilizzati in modo inaspettato. Conviene memorizzare lo state nel modello dati o in un ViewModel.

Riordinare Elementi in LazyVStack (iOS 27)

Prima di iOS 27, il drag-to-reorder richiedeva List con onMove(perform:). Le nuove API reorderable funzionano con qualsiasi container, inclusi LazyVStack e LazyVGrid.

ReorderableStack.swiftswift
import SwiftUI

struct ReorderableCardList: View {
    @State private var cards: [Card] = Card.sampleData

    var body: some View {
        ScrollView {
            LazyVStack(spacing: 12) {
                ForEach(cards) { card in
                    CardView(card: card)
                }
                .reorderable()  // Mark content as draggable
            }
            .padding()
            .reorderContainer(for: Card.self) { difference in
                // Apply the reorder to the model
                cards.apply(difference: difference)
            }
        }
    }
}

struct Card: Identifiable {
    let id: UUID
    var title: String
    var color: Color

    static var sampleData: [Card] {
        [
            Card(id: UUID(), title: "Design Review", color: .blue),
            Card(id: UUID(), title: "Code Sprint", color: .green),
            Card(id: UUID(), title: "Testing Phase", color: .orange)
        ]
    }
}

struct CardView: View {
    let card: Card

    var body: some View {
        Text(card.title)
            .font(.headline)
            .frame(maxWidth: .infinity)
            .padding()
            .background(card.color.opacity(0.2))
            .cornerRadius(12)
    }
}

// Extension to apply ReorderDifference
extension Array where Element: Identifiable {
    mutating func apply(difference: ReorderDifference<Element.ID>) {
        // Move items from sources to destination
        let movedItems = difference.sources.compactMap { id in
            self.first { $0.id == id }
        }

        // Remove from original positions
        self.removeAll { item in
            difference.sources.contains(item.id)
        }

        // Insert at destination
        if let destinationIndex = self.firstIndex(where: { $0.id == difference.destination }) {
            self.insert(contentsOf: movedItems, at: destinationIndex)
        } else {
            self.append(contentsOf: movedItems)
        }
    }
}

SwiftUI gestisce la preview del drag, il placeholder di inserimento e l'animazione del drop. L'oggetto ReorderDifference fornisce gli ID sorgente e la destinazione, lasciando gli aggiornamenti del model al codice dell'applicazione.

Sezioni e Header Ottimizzati

Organizzare il contenuto in sezioni migliora la leggibilità ma può impattare le performance se mal implementato. Gli header pinnati e la gestione delle sezioni richiedono particolare attenzione.

OptimizedSections.swiftswift
// Grouped data model
struct GroupedContacts {
    let letter: String
    let contacts: [Contact]
}

// View with optimized sections
struct SectionedContactList: View {
    let groupedContacts: [GroupedContacts]

    var body: some View {
        ScrollView {
            LazyVStack(spacing: 0, pinnedViews: [.sectionHeaders]) {
                ForEach(groupedContacts, id: .letter) { group in
                    Section {
                        // Section content
                        ForEach(group.contacts) { contact in
                            ContactRow(contact: contact)
                                .padding(.horizontal)
                                .padding(.vertical, 8)

                            // Custom separator
                            if contact.id != group.contacts.last?.id {
                                Divider()
                                    .padding(.leading, 68)
                            }
                        }
                    } header: {
                        // Optimized pinned header
                        SectionHeader(title: group.letter)
                    }
                }
            }
        }
    }
}

// Lightweight header for performance
struct SectionHeader: View {
    let title: String

    var body: some View {
        Text(title)
            .font(.headline)
            .foregroundStyle(.secondary)
            .frame(maxWidth: .infinity, alignment: .leading)
            .padding(.horizontal)
            .padding(.vertical, 8)
            .background(.ultraThinMaterial)
    }
}

// Optimized grouping function
extension Array where Element == Contact {
    func groupedByFirstLetter() -> [GroupedContacts] {
        // Dictionary for O(n) grouping
        var groups: [String: [Contact]] = [:]

        for contact in self {
            let letter = String(contact.name.prefix(1)).uppercased()
            groups[letter, default: []].append(contact)
        }

        // Sort groups alphabetically
        return groups
            .map { GroupedContacts(letter: $0.key, contacts: $0.value) }
            .sorted { $0.letter < $1.letter }
    }
}

Gli header pinnati (pinnedViews: [.sectionHeaders]) restano visibili durante lo scroll, migliorando la navigazione in liste lunghe.

Paginazione e Scroll Infinito

Per grandi dataset, la paginazione evita di caricare tutti i dati in memoria. L'implementazione deve essere trasparente per l'utente.

InfiniteScrolling.swiftswift
// ViewModel handling pagination
@Observable
final class PaginatedListViewModel {
    private(set) var items: [Contact] = []
    private(set) var isLoading = false
    private(set) var hasMorePages = true

    private var currentPage = 0
    private let pageSize = 20
    private let dataService: ContactDataService

    init(dataService: ContactDataService) {
        self.dataService = dataService
    }

    func loadInitialData() async {
        guard items.isEmpty else { return }
        await loadNextPage()
    }

    func loadMoreIfNeeded(currentItem: Contact) async {
        // Trigger loading when approaching the end
        guard let index = items.firstIndex(where: { $0.id == currentItem.id }) else {
            return
        }

        // Load 5 items before the end
        let thresholdIndex = items.count - 5

        if index >= thresholdIndex {
            await loadNextPage()
        }
    }

    private func loadNextPage() async {
        guard !isLoading, hasMorePages else { return }

        isLoading = true
        defer { isLoading = false }

        do {
            let newItems = try await dataService.fetchContacts(
                page: currentPage,
                limit: pageSize
            )

            items.append(contentsOf: newItems)
            currentPage += 1
            hasMorePages = newItems.count == pageSize
        } catch {
            // Handle error
        }
    }
}

// View with infinite scroll
struct InfiniteContactList: View {
    @State private var viewModel: PaginatedListViewModel

    init(dataService: ContactDataService) {
        _viewModel = State(initialValue: PaginatedListViewModel(dataService: dataService))
    }

    var body: some View {
        List {
            ForEach(viewModel.items) { contact in
                ContactRow(contact: contact)
                    .task {
                        // Check if more loading needed
                        await viewModel.loadMoreIfNeeded(currentItem: contact)
                    }
            }

            // Loading indicator at end of list
            if viewModel.isLoading {
                HStack {
                    Spacer()
                    ProgressView()
                    Spacer()
                }
                .padding()
            }
        }
        .task {
            await viewModel.loadInitialData()
        }
    }
}

// Protocol for data service
protocol ContactDataService {
    func fetchContacts(page: Int, limit: Int) async throws -> [Contact]
}

Questo pattern garantisce un caricamento fluido senza bloccare l'interfaccia e permette una gestione efficiente della memoria.

Pronto a superare i tuoi colloqui su iOS?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

Profiling con Instruments

Identificare i problemi di performance richiede strumenti di misurazione precisi. Instruments offre diversi template adatti a SwiftUI.

ProfilingHelpers.swiftswift
// Measurement points for debugging
struct PerformanceMonitor {
    // Measure view creation time
    static func measureViewCreation<T: View>(
        _ name: String,
        @ViewBuilder content: () -> T
    ) -> T {
        let start = CFAbsoluteTimeGetCurrent()
        let view = content()
        let elapsed = CFAbsoluteTimeGetCurrent() - start

        #if DEBUG
        if elapsed > 0.016 {  // More than 16ms = frame drop
            print("[(name)] View creation took (elapsed * 1000)ms")
        }
        #endif

        return view
    }
}

// Extension to trace renders
extension View {
    func debugRender(_ label: String) -> some View {
        #if DEBUG
        let _ = Self._printChanges()
        print("Rendering: (label)")
        #endif
        return self
    }

    func measureRender(_ label: String) -> some View {
        modifier(RenderMeasureModifier(label: label))
    }
}

struct RenderMeasureModifier: ViewModifier {
    let label: String
    @State private var renderCount = 0

    func body(content: Content) -> some View {
        content
            .onAppear {
                renderCount += 1
                #if DEBUG
                print("[(label)] Render count: (renderCount)")
                #endif
            }
    }
}

Checklist di Ottimizzazione con Instruments

Per un profiling efficace delle liste SwiftUI:

  1. Time Profiler: identificare le funzioni che consumano più CPU
  2. Allocations: verificare la crescita della memoria durante lo scroll
  3. SwiftUI Instrument: visualizzare le valutazioni di body
  4. Core Animation: rilevare cali di frame
InstrumentsExample.swiftswift
// Instrumented view for profiling
struct ProfiledContactList: View {
    let contacts: [Contact]

    var body: some View {
        let _ = Self._printChanges()  // Shows changes triggering re-render

        List {
            ForEach(contacts) { contact in
                ContactRow(contact: contact)
                    .measureRender("ContactRow-(contact.id)")
            }
        }
    }
}
Self._printChanges()

Questa API di debugging di SwiftUI stampa in console quali proprietà sono cambiate e hanno innescato una rivalutazione del body. Essenziale per identificare rendering inutili.

Ottimizzazioni Avanzate con drawingGroup

Per view complesse con molti effetti visivi, drawingGroup() può migliorare significativamente le performance rasterizzando la view in un layer Metal.

DrawingGroupOptimization.swiftswift
// Cell with complex visual effects
struct ComplexVisualRow: View {
    let item: Item

    var body: some View {
        HStack(spacing: 16) {
            // Circle with gradient and shadow
            Circle()
                .fill(
                    RadialGradient(
                        colors: [.blue, .purple, .pink],
                        center: .center,
                        startRadius: 0,
                        endRadius: 25
                    )
                )
                .frame(width: 50, height: 50)
                .shadow(color: .purple.opacity(0.5), radius: 8, y: 4)

            VStack(alignment: .leading, spacing: 4) {
                Text(item.name)
                    .font(.headline)

                // Progress bar with gradient
                GeometryReader { geometry in
                    Capsule()
                        .fill(Color.gray.opacity(0.2))
                        .overlay(alignment: .leading) {
                            Capsule()
                                .fill(
                                    LinearGradient(
                                        colors: [.green, .yellow, .orange],
                                        startPoint: .leading,
                                        endPoint: .trailing
                                    )
                                )
                                .frame(width: geometry.size.width * item.progress)
                        }
                }
                .frame(height: 8)
            }
        }
        .padding()
        // Rasterization for performance
        .drawingGroup()
    }
}

// List using optimized cells
struct OptimizedComplexList: View {
    let items: [Item]

    var body: some View {
        ScrollView {
            LazyVStack(spacing: 8) {
                ForEach(items) { item in
                    ComplexVisualRow(item: item)
                }
            }
            .padding()
        }
    }
}

struct Item: Identifiable {
    let id: UUID
    let name: String
    let progress: Double
}

drawingGroup() è particolarmente efficace per view che combinano gradienti, ombre ed effetti blur.

Fonti

Conclusione

L'ottimizzazione delle liste SwiftUI si basa su una profonda comprensione dei meccanismi di lazy loading, riciclo e confronto delle view. iOS 27 ha portato miglioramenti significativi con il caching nativo di AsyncImage e le API container-agnostiche per swipe actions e riordino.

Checklist delle Performance SwiftUI

  • Usare LazyVStack o List invece di VStack per le collezioni
  • Implementare Identifiable con ID stabili e univoci
  • Adottare Equatable per celle complesse
  • Sfruttare il caching nativo di AsyncImage di iOS 27, o implementare caching personalizzato per versioni precedenti
  • Precaricare i dati con prefetching intelligente
  • Scegliere List per il riciclo delle celle o LazyVStack per layout personalizzati (entrambi ora supportano swipe actions e riordino)
  • Usare pinnedViews per gli header di sezione
  • Implementare la paginazione per grandi dataset
  • Fare profiling regolare con Instruments
  • Applicare drawingGroup() alle view con effetti visivi complessi

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

Sfida del giorno

Sapresti trovare il bug in iOS?

Uno snippet reale, un bug nascosto, un tentativo al giorno. Senza account per provare.

Anthony Fillion-Maillet

Scritto da

Anthony Fillion-Maillet

Fondatore di SharpSkill

Sviluppatore fullstack da oltre 10 anni. Guida SharpSkill e risponde di tutto ciò che vi viene pubblicato.

Aggiornato il 19 agosto 2026

Tag

#swiftui
#ios
#performance
#lazyvstack
#swift

Condividi

Articoli correlati