SwiftUI Performans: LazyVStack ve Karmaşık Listeleri Optimize Etme

LazyVStack ve SwiftUI listeleri için optimizasyon teknikleri. Bellek tüketimini azaltın, kaydırma performansını iyileştirin ve sık karşılaşılan tuzakları önleyin.

SwiftUI LazyVStack ve karmaşık liste performans optimizasyonu

Listeler, iOS uygulamalarında en sık kullanılan bileşenlerden birini temsil eder. SwiftUI'nin LazyVStack ve List bileşenleri veri koleksiyonlarını göstermek için performanslı çözümler sunar, ancak yanlış kullanım kullanıcı deneyimini hızla bozabilir. Bu bileşenlerin iç mekanizmalarını anlamak, sık karşılaşılan tuzakları önlemeye ve akıcı arayüzler oluşturmaya yardımcı olur.

Bu makalenin kapsamı

Bu makale SwiftUI listeleri için temel optimizasyon tekniklerini sunar: lazy loading, view geri dönüşümü, tanımlayıcı yönetimi ve büyük veri kümeleri için gelişmiş desenler. Yeni yeniden sıralama ve swipe action API'leri ile iOS 27 için güncellendi.

SwiftUI'de Lazy Loading'i Anlamak

Lazy loading, view'ları yalnızca ekranda görünür hale geldiklerinde örneklendirir. Tüm alt view'ları hemen oluşturan VStack'in aksine, LazyVStack bu oluşturma işlemini erteler ve bellek tüketimini ile başlangıç render süresini önemli ölçüde azaltır.

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

Performans farkı yalnızca birkaç yüz öğeyle bile belirgin hale gelir. 10.000 öğeyle VStack başlatılması birkaç saniye sürebilirken LazyVStack anında kalır.

Lazy Loading Etkisini Ölçmek

Instruments, bellek ve CPU kullanımını hassas bir şekilde ölçmeye olanak tanır. İşte farkı gösteren bir test view'i:

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

VStack ile çalıştırıldığında, 10.000 logun tamamı hemen görünür. LazyVStack ile, yalnızca görünür öğeler (ekran boyutuna bağlı olarak yaklaşık 15-20) başlangıçta loglanır ve kaydırma sırasında daha fazlası ortaya çıkar.

Tutma davranışı

LazyVStack, oluşturulan view'ları göründükten sonra bellekte tutar. Hücreleri aktif olarak geri dönüştüren List'in aksine, LazyVStack içindeki view'lar üst bileşen yok edilene kadar kalır.

Kararlı Tanımlayıcıların Önemi

Tanımlayıcılar SwiftUI liste güncellemelerinin merkezi mekanizmasını oluşturur. Kararsız bir tanımlayıcı gereksiz view yeniden oluşturmalarına neden olur ve yanlış animasyonlar veya kaydırma konumu kaybı gibi görsel hatalara yol açabilir.

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

Benzersiz ve kalıcı bir tanımlayıcı kullanmak, SwiftUI'nin güncellemeler, animasyonlar ve karşılaştırmalar sırasında öğeleri doğru şekilde ayırt edebilmesini garantiler.

Equatable ile Hücre Optimizasyonu

SwiftUI yeniden render gerekip gerekmediğini belirlemek için view'ları karşılaştırır. Varsayılan olarak bu karşılaştırma reflection kullanır ki bu da maliyetli olabilir. Equatable uygulamak optimize edilmiş ve açık karşılaştırma sağlar.

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

Bu optimizasyon, özellikle hesaplama veya görsel içeren hücrelerde hızlı kaydırma sırasında CPU yükünü önemli ölçüde azaltır.

iOS mülakatlarında başarılı olmaya hazır mısın?

İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.

Asenkron Görsel Yüklemenin Yönetimi

Görseller listelerde sıklıkla performans darboğazı haline gelir. iOS 27, birçok senaryoda manuel önbelleğe alma ihtiyacını ortadan kaldırarak varsayılan olarak AsyncImage için standart HTTP önbelleğe alma özelliğini tanıttı. iOS 26 ve önceki sürümleri hedefleyen veya önbelleğe alma davranışı üzerinde hassas kontrol gerektiren uygulamalar için özel bir uygulama değerli olmaya devam eder.

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

iOS 27 ve sonrası için standart AsyncImage HTTP cache header'larını otomatik olarak dikkate alır. Özel gereksinimler için asyncImageURLSession(_:) modifier'ı aracılığıyla özel bir URLCache uygulanabilir.

Öngörü için Akıllı Prefetching

Çok uzun listeler için prefetching görselleri görünür hale gelmeden önce yükler:

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

List ve LazyVStack Arasında Seçim

iOS 27, List ile LazyVStack arasındaki farkı önemli ölçüde azalttı. Daha önce, List yerel swipe eylemleri ve sürükleyerek yeniden sıralama için tek seçenekti. Yeni .swipeActionsContainer() ve .reorderable() API'leri bu yetenekleri herhangi bir konteynere taşıyor.

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

Seçim artık özellik kullanılabilirliğine değil görsel gereksinimlere bağlıdır:

GereksinimÖnerilen Konteyner
Bellek verimliliği için hücre geri dönüşümüList
Özel boşluk ve paddingLazyVStack
Yerleşik ayırıcılar ve girintilerList
Kart tabanlı veya standart dışı düzenlerLazyVStack
Yapışkan davranışlı bölüm başlıklarıHer ikisi (pinnedViews kullanın)
Swipe eylemleriHer ikisi (iOS 27+)
Sürükleyerek yeniden sıralamaHer ikisi (iOS 27+)
Hücre geri dönüşümü

List hücreleri aktif olarak geri dönüştürür ve bu, yerel state (@State) ile sorunlara neden olabilir. List hücrelerindeki @State değerleri beklenmedik şekilde yeniden kullanılabilir. State'i veri modelinde veya bir ViewModel'de saklamak tercih edilmelidir.

LazyVStack'te Öğeleri Yeniden Sıralama (iOS 27)

iOS 27'den önce, sürükleyerek yeniden sıralama onMove(perform:) ile List gerektiriyordu. Yeni reorderable API'leri LazyVStack ve LazyVGrid dahil herhangi bir konteynerle çalışır.

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 sürükleme önizlemesini, ekleme placeholder'ını ve bırakma animasyonunu yönetir. ReorderDifference nesnesi kaynak ID'lerini ve hedefi sağlar, model güncellemelerini uygulama koduna bırakır.

Optimize Edilmiş Bölümler ve Başlıklar

İçeriği bölümler halinde organize etmek okunabilirliği artırır ancak kötü uygulanırsa performansı etkileyebilir. Sabitlenmiş başlıklar ve bölüm yönetimi özel dikkat gerektirir.

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

Sabitlenmiş başlıklar (pinnedViews: [.sectionHeaders]) kaydırma sırasında görünür kalır ve uzun listelerde gezinmeyi iyileştirir.

Sayfalama ve Sonsuz Kaydırma

Büyük veri kümeleri için sayfalama tüm verileri belleğe yüklemekten kaçınır. Uygulama kullanıcı için şeffaf olmalıdır.

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

Bu desen, arayüzü engellemeden akıcı yükleme sağlar ve verimli bellek yönetimine olanak tanır.

iOS mülakatlarında başarılı olmaya hazır mısın?

İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.

Instruments ile Profilleme

Performans sorunlarını tanımlamak hassas ölçüm araçları gerektirir. Instruments, SwiftUI'ye uygun çeşitli şablonlar sunar.

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

Instruments Optimizasyon Kontrol Listesi

Etkili SwiftUI liste profillemesi için:

  1. Time Profiler: en çok CPU tüketen fonksiyonları tanımlayın
  2. Allocations: kaydırma sırasında bellek artışını doğrulayın
  3. SwiftUI Instrument: body değerlendirmelerini görselleştirin
  4. Core Animation: kare düşüşlerini tespit edin
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()

Bu SwiftUI hata ayıklama API'si, hangi özelliklerin değiştiğini ve body yeniden değerlendirmesini tetiklediğini konsola yazdırır. Gereksiz yeniden render'ları tanımlamak için vazgeçilmezdir.

drawingGroup ile İleri Düzey Optimizasyonlar

Birçok görsel efekt içeren karmaşık view'lar için drawingGroup(), view'ı bir Metal katmanına rasterize ederek performansı önemli ölçüde artırabilir.

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(), gradyanlar, gölgeler ve bulanıklık efektlerini birleştiren view'larda özellikle etkilidir.

Kaynaklar

Sonuç

SwiftUI liste optimizasyonu, lazy loading, geri dönüşüm ve view karşılaştırma mekanizmalarının derin bir anlayışına dayanır. iOS 27, yerel AsyncImage önbelleğe alma ve konteynerden bağımsız swipe eylemleri ve yeniden sıralama API'leri ile önemli iyileştirmeler getirdi.

SwiftUI Performans Kontrol Listesi

  • Koleksiyonlar için VStack yerine LazyVStack veya List kullanın
  • Kararlı, benzersiz ID'lerle Identifiable uygulayın
  • Karmaşık hücreler için Equatable benimseyin
  • iOS 27'nin yerel AsyncImage önbelleğe almasını kullanın veya önceki sürümler için özel önbelleğe alma uygulayın
  • Akıllı prefetching ile verileri önceden yükleyin
  • Hücre geri dönüşümü için List veya özel düzenler için LazyVStack seçin (her ikisi artık swipe eylemleri ve yeniden sıralamayı destekliyor)
  • Bölüm başlıkları için pinnedViews kullanın
  • Büyük veri kümeleri için sayfalama uygulayın
  • Düzenli olarak Instruments ile profilleyin
  • Karmaşık görsel efektlere sahip view'lara drawingGroup() uygulayın

Pratik yapmaya başla!

Mülakat simülatörleri ve teknik testlerle bilgini test et.

Günün meydan okuması

iOS kodundaki hatayı bulabilir misin?

Gerçek bir kod parçası, gizli bir hata, günde bir deneme. Denemek için hesap gerekmez.

Anthony Fillion-Maillet

Yazan:

Anthony Fillion-Maillet

SharpSkill kurucusu

10 yılı aşkın süredir fullstack geliştirici. SharpSkill’i yönetiyor ve burada yayımlanan her şeyden sorumlu.

19 Ağustos 2026 tarihinde güncellendi

Etiketler

#swiftui
#ios
#performance
#lazyvstack
#swift

Paylaş

İlgili makaleler