Wydajność SwiftUI: Optymalizacja LazyVStack i Złożonych List

Techniki optymalizacji LazyVStack i list SwiftUI. Zmniejszanie zużycia pamięci, poprawa wydajności przewijania i unikanie typowych pułapek.

Optymalizacja wydajności SwiftUI LazyVStack i złożonych list

Listy stanowią jeden z najczęściej używanych komponentów w aplikacjach iOS. LazyVStack i List w SwiftUI oferują wydajne rozwiązania do wyświetlania kolekcji danych, ale niewłaściwe użycie może szybko pogorszyć doświadczenie użytkownika. Zrozumienie wewnętrznego działania tych komponentów pomaga uniknąć częstych pułapek i budować płynne interfejsy.

Co obejmuje ten artykuł

Artykuł przedstawia kluczowe techniki optymalizacji list SwiftUI: lazy loading, recykling widoków, zarządzanie identyfikatorami oraz zaawansowane wzorce dla dużych zbiorów danych. Zaktualizowano dla iOS 27 z nowymi API do zmiany kolejności i akcji swipe.

Zrozumienie Lazy Loading w SwiftUI

Lazy loading tworzy widoki dopiero wtedy, gdy stają się widoczne na ekranie. W przeciwieństwie do VStack, który tworzy wszystkie widoki potomne natychmiast, LazyVStack opóźnia tę operację, drastycznie zmniejszając zużycie pamięci i czas początkowego renderowania.

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)
                }
            }
        }
    }
}

Różnica wydajności staje się znacząca już przy kilkuset elementach. Przy 10 000 elementach VStack może uruchamiać się przez kilka sekund, podczas gdy LazyVStack pozostaje natychmiastowy.

Pomiar Wpływu Lazy Loading

Instruments umożliwia precyzyjny pomiar zużycia pamięci i CPU. Oto widok testowy ilustrujący różnicę:

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()
    }
}

Podczas uruchamiania z VStack wszystkie 10 000 logów pojawia się natychmiast. Z LazyVStack tylko widoczne elementy (około 15-20 w zależności od rozmiaru ekranu) są początkowo logowane, a kolejne pojawiają się podczas przewijania.

Zachowanie retencji

LazyVStack przechowuje utworzone widoki w pamięci po ich pojawieniu się. W przeciwieństwie do List, który aktywnie recyklinguje komórki, widoki w LazyVStack pozostają aż do zniszczenia komponentu nadrzędnego.

Znaczenie Stabilnych Identyfikatorów

Identyfikatory stanowią centralny mechanizm aktualizacji list SwiftUI. Niestabilny identyfikator powoduje niepotrzebne odtwarzanie widoków i może wywoływać błędy wizualne, takie jak nieprawidłowe animacje lub utratę pozycji przewijania.

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)
            }
        }
    }
}

Używanie unikalnego i trwałego identyfikatora zapewnia, że SwiftUI poprawnie rozróżnia elementy podczas aktualizacji, animacji i porównań.

Optymalizacja Komórek z Equatable

SwiftUI porównuje widoki, aby określić, czy konieczne jest ponowne renderowanie. Domyślnie to porównanie używa refleksji, co może być kosztowne. Implementacja Equatable pozwala na zoptymalizowane i jawne porównanie.

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))
            }
        }
    }
}

Ta optymalizacja znacznie zmniejsza obciążenie CPU podczas szybkiego przewijania, szczególnie przy komórkach zawierających obliczenia lub obrazy.

Gotowy na rozmowy o iOS?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Zarządzanie Asynchronicznym Ładowaniem Obrazów

Obrazy często stają się wąskim gardłem wydajności w listach. iOS 27 wprowadził standardowe buforowanie HTTP dla AsyncImage domyślnie, eliminując potrzebę ręcznego buforowania w wielu scenariuszach. Dla aplikacji celujących w iOS 26 i wcześniejsze wersje lub wymagających precyzyjnej kontroli nad buforowaniem, własna implementacja pozostaje wartościowa.

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)
            }
        }
    }
}

Dla iOS 27 i nowszych, standardowy AsyncImage automatycznie respektuje nagłówki cache HTTP. Niestandardowy URLCache można zastosować przez modyfikator asyncImageURLSession(_:) dla specyficznych wymagań.

Inteligentne Prefetching dla Antycypacji

Dla bardzo długich list prefetching ładuje obrazy zanim staną się widoczne:

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 { /bin/bash.avatarURL }
        )

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

Wybór między List a LazyVStack

iOS 27 znacząco zmniejszył różnicę między List a LazyVStack. Wcześniej List był jedyną opcją dla natywnych akcji swipe i przeciągania do zmiany kolejności. Nowe API .swipeActionsContainer() i .reorderable() przenoszą te możliwości do dowolnego kontenera.

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 { /bin/bash.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 { /bin/bash.id == post.id }
    }
}

Wybór zależy teraz od wymagań wizualnych, a nie dostępności funkcji:

WymaganieZalecany kontener
Recykling komórek dla efektywności pamięciList
Niestandardowe odstępy i paddingLazyVStack
Wbudowane separatory i wcięciaList
Układy kartowe lub niestandardoweLazyVStack
Nagłówki sekcji z zachowaniem stickyOba (użyj pinnedViews)
Akcje swipeOba (iOS 27+)
Przeciąganie do zmiany kolejnościOba (iOS 27+)
Recykling komórek

List aktywnie recyklinguje komórki, co może powodować problemy z lokalnym stanem (@State). Wartości @State w komórkach List mogą być nieoczekiwanie ponownie używane. Lepiej przechowywać stan w modelu danych lub w ViewModel.

Zmiana Kolejności Elementów w LazyVStack (iOS 27)

Przed iOS 27 przeciąganie do zmiany kolejności wymagało List z onMove(perform:). Nowe API reorderable działają z dowolnym kontenerem, włącznie z LazyVStack i 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 { /bin/bash.id == id }
        }

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

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

SwiftUI obsługuje podgląd przeciągania, placeholder wstawiania i animację upuszczania. Obiekt ReorderDifference dostarcza ID źródłowe i miejsce docelowe, pozostawiając aktualizacje modelu kodowi aplikacji.

Zoptymalizowane Sekcje i Nagłówki

Organizowanie treści w sekcje poprawia czytelność, ale może wpływać na wydajność przy złej implementacji. Przypięte nagłówki i zarządzanie sekcjami wymagają szczególnej uwagi.

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: /bin/bash.key, contacts: /bin/bash.value) }
            .sorted { /bin/bash.letter < .letter }
    }
}

Przypięte nagłówki (pinnedViews: [.sectionHeaders]) pozostają widoczne podczas przewijania, poprawiając nawigację w długich listach.

Paginacja i Nieskończone Przewijanie

Dla dużych zbiorów danych paginacja unika ładowania wszystkich danych do pamięci. Implementacja powinna być przezroczysta dla użytkownika.

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: { /bin/bash.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]
}

Ten wzorzec zapewnia płynne ładowanie bez blokowania interfejsu i umożliwia efektywne zarządzanie pamięcią.

Gotowy na rozmowy o iOS?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Profilowanie z Instruments

Identyfikacja problemów z wydajnością wymaga precyzyjnych narzędzi pomiarowych. Instruments oferuje kilka szablonów odpowiednich dla 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
            }
    }
}

Lista Kontrolna Optymalizacji z Instruments

Dla skutecznego profilowania list SwiftUI:

  1. Time Profiler: identyfikacja funkcji zużywających najwięcej CPU
  2. Allocations: weryfikacja wzrostu pamięci podczas przewijania
  3. SwiftUI Instrument: wizualizacja ewaluacji body
  4. Core Animation: wykrywanie spadków klatek
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()

To API debugowania SwiftUI wyświetla w konsoli, które właściwości się zmieniły i wywołały ponowną ewaluację body. Niezbędne do identyfikacji niepotrzebnych renderowań.

Zaawansowane Optymalizacje z drawingGroup

Dla złożonych widoków z wieloma efektami wizualnymi drawingGroup() może znacznie poprawić wydajność, rastrując widok do warstwy 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() jest szczególnie skuteczny dla widoków łączących gradienty, cienie i efekty rozmycia.

Źródła

Podsumowanie

Optymalizacja list SwiftUI opiera się na głębokim zrozumieniu mechanizmów lazy loading, recyklingu i porównywania widoków. iOS 27 przyniósł znaczące ulepszenia z natywnym buforowaniem AsyncImage oraz niezależnymi od kontenera akcjami swipe i API do zmiany kolejności.

Lista Kontrolna Wydajności SwiftUI

  • Używać LazyVStack lub List zamiast VStack dla kolekcji
  • Implementować Identifiable ze stabilnymi, unikalnymi ID
  • Stosować Equatable dla złożonych komórek
  • Wykorzystywać natywne buforowanie AsyncImage w iOS 27 lub implementować własne buforowanie dla wcześniejszych wersji
  • Wczytywać dane wstępnie z inteligentnym prefetching
  • Wybierać List dla recyklingu komórek lub LazyVStack dla niestandardowych układów (oba teraz obsługują akcje swipe i zmianę kolejności)
  • Używać pinnedViews dla nagłówków sekcji
  • Implementować paginację dla dużych zbiorów danych
  • Regularnie profilować z Instruments
  • Stosować drawingGroup() do widoków ze złożonymi efektami wizualnymi

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Wyzwanie dnia

Znajdziesz błąd w iOS?

Prawdziwy fragment kodu, ukryty błąd, jedna próba dziennie. Bez konta, żeby spróbować.

Anthony Fillion-Maillet

Autor:

Anthony Fillion-Maillet

Założyciel SharpSkill

Programista fullstack od ponad 10 lat. Prowadzi SharpSkill i odpowiada za wszystko, co się tu ukazuje.

Zaktualizowano 19 sierpnia 2026

Tagi

#swiftui
#ios
#performance
#lazyvstack
#swift

Udostępnij

Powiązane artykuły