ประสิทธิภาพ SwiftUI: การปรับแต่ง LazyVStack และรายการที่ซับซ้อน

เทคนิคการปรับแต่งสำหรับ LazyVStack และรายการ SwiftUI ลดการใช้หน่วยความจำ ปรับปรุงประสิทธิภาพการเลื่อน และหลีกเลี่ยงข้อผิดพลาดที่พบบ่อย

การปรับแต่งประสิทธิภาพ SwiftUI LazyVStack และรายการที่ซับซ้อน

รายการเป็นหนึ่งในส่วนประกอบที่ใช้บ่อยที่สุดในแอปพลิเคชัน iOS LazyVStack และ List ของ SwiftUI ให้โซลูชันที่มีประสิทธิภาพในการแสดงคอลเลกชันข้อมูล แต่การใช้งานที่ไม่ถูกต้องอาจลดประสบการณ์ผู้ใช้ได้อย่างรวดเร็ว การเข้าใจกลไกภายในของส่วนประกอบเหล่านี้ช่วยหลีกเลี่ยงข้อผิดพลาดที่พบบ่อยและสร้างอินเทอร์เฟซที่ลื่นไหล

สิ่งที่บทความนี้ครอบคลุม

บทความนี้นำเสนอเทคนิคการปรับแต่งที่จำเป็นสำหรับรายการ SwiftUI: lazy loading การรีไซเคิลวิว การจัดการตัวระบุ และรูปแบบขั้นสูงสำหรับชุดข้อมูลขนาดใหญ่ อัปเดตสำหรับ iOS 27 พร้อม API การจัดเรียงใหม่และ swipe action ใหม่

ทำความเข้าใจ Lazy Loading ใน SwiftUI

Lazy loading จะสร้างวิวเฉพาะเมื่อปรากฏบนหน้าจอเท่านั้น ต่างจาก VStack ที่สร้างวิวลูกทั้งหมดทันที LazyVStack จะเลื่อนการสร้างนี้ออกไป ลดการใช้หน่วยความจำและเวลาในการเรนเดอร์เริ่มต้นได้อย่างมาก

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

ความแตกต่างของประสิทธิภาพจะเห็นได้ชัดเจนแม้กับเพียงไม่กี่ร้อยรายการ ด้วย 10,000 รายการ VStack อาจใช้เวลาหลายวินาทีในการเริ่มต้น ในขณะที่ LazyVStack ยังคงตอบสนองทันที

การวัดผลกระทบของ Lazy Loading

Instruments ช่วยให้สามารถวัดการใช้หน่วยความจำและ CPU ได้อย่างแม่นยำ นี่คือวิวทดสอบที่แสดงความแตกต่าง:

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 ล็อกทั้งหมด 10,000 รายการจะปรากฏทันที ด้วย LazyVStack มีเพียงรายการที่มองเห็นได้ (ประมาณ 15-20 ขึ้นอยู่กับขนาดหน้าจอ) ที่ถูกล็อกในตอนแรก โดยมีรายการเพิ่มเติมปรากฏขึ้นเมื่อเลื่อน

พฤติกรรมการคงอยู่

LazyVStack เก็บวิวที่สร้างไว้ในหน่วยความจำหลังจากที่ปรากฏ ต่างจาก List ที่รีไซเคิลเซลล์อย่างแอคทีฟ วิวใน LazyVStack จะคงอยู่จนกว่าส่วนประกอบหลักจะถูกทำลาย

ความสำคัญของตัวระบุที่เสถียร

ตัวระบุเป็นกลไกหลักของการอัปเดตรายการ SwiftUI ตัวระบุที่ไม่เสถียรทำให้เกิดการสร้างวิวใหม่ที่ไม่จำเป็นและอาจกระตุ้นบั๊กทางสายตา เช่น แอนิเมชันที่ผิดพลาดหรือการสูญเสียตำแหน่งการเลื่อน

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

การใช้ตัวระบุที่ไม่ซ้ำกันและคงทนช่วยให้ SwiftUI สามารถแยกแยะองค์ประกอบได้อย่างถูกต้องระหว่างการอัปเดต แอนิเมชัน และการเปรียบเทียบ

การปรับแต่งเซลล์ด้วย Equatable

SwiftUI เปรียบเทียบวิวเพื่อพิจารณาว่าจำเป็นต้องเรนเดอร์ใหม่หรือไม่ โดยค่าเริ่มต้น การเปรียบเทียบนี้ใช้ reflection ซึ่งอาจมีค่าใช้จ่ายสูง การใช้ Equatable ช่วยให้สามารถเปรียบเทียบได้อย่างมีประสิทธิภาพและชัดเจน

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

การปรับแต่งนี้ลดภาระ CPU อย่างมีนัยสำคัญในระหว่างการเลื่อนเร็ว โดยเฉพาะกับเซลล์ที่มีการคำนวณหรือรูปภาพ

พร้อมที่จะพิชิตการสัมภาษณ์ iOS แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

การจัดการการโหลดรูปภาพแบบอะซิงโครนัส

รูปภาพมักจะกลายเป็นจุดคอขวดด้านประสิทธิภาพในรายการ iOS 27 ได้แนะนำ การแคช HTTP มาตรฐานสำหรับ AsyncImage เป็นค่าเริ่มต้น ขจัดความจำเป็นในการแคชด้วยตนเองในหลายสถานการณ์ สำหรับแอปพลิเคชันที่กำหนดเป้าหมาย iOS 26 และก่อนหน้า หรือต้องการการควบคุมการแคชอย่างละเอียด การใช้งานแบบกำหนดเองยังคงมีคุณค่า

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 และใหม่กว่า AsyncImage มาตรฐานจะเคารพ header แคช HTTP โดยอัตโนมัติ สามารถใช้ URLCache แบบกำหนดเองผ่าน modifier asyncImageURLSession(_:) สำหรับข้อกำหนดเฉพาะ

Prefetching อัจฉริยะเพื่อการคาดการณ์

สำหรับรายการที่ยาวมาก prefetching จะโหลดรูปภาพก่อนที่จะปรากฏ:

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 และ LazyVStack

iOS 27 ได้ลดช่องว่างระหว่าง List และ LazyVStack อย่างมีนัยสำคัญ ก่อนหน้านี้ List เป็นตัวเลือกเดียวสำหรับ swipe action native และ drag-to-reorder API .swipeActionsContainer() และ .reorderable() ใหม่ นำความสามารถเหล่านี้มาสู่ 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 { /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 }
    }
}

ตัวเลือกตอนนี้ขึ้นอยู่กับข้อกำหนดทางภาพมากกว่าความพร้อมใช้งานของฟีเจอร์:

ข้อกำหนดContainer ที่แนะนำ
การรีไซเคิลเซลล์เพื่อประสิทธิภาพหน่วยความจำList
Spacing และ padding แบบกำหนดเองLazyVStack
Separator และ inset ในตัวList
เลย์เอาต์แบบการ์ดหรือไม่ได้มาตรฐานLazyVStack
Header section พร้อมพฤติกรรม stickyทั้งสอง (ใช้ pinnedViews)
Swipe actionsทั้งสอง (iOS 27+)
Drag-to-reorderทั้งสอง (iOS 27+)
การรีไซเคิลเซลล์

List รีไซเคิลเซลล์อย่างแอคทีฟ ซึ่งอาจทำให้เกิดปัญหากับ state ภายใน (@State) ค่า @State ในเซลล์ของ List อาจถูกใช้ซ้ำโดยไม่คาดคิด ควรเก็บ state ไว้ในโมเดลข้อมูลหรือใน ViewModel

การจัดเรียงรายการใน LazyVStack (iOS 27)

ก่อน iOS 27 การ drag-to-reorder ต้องใช้ List พร้อม onMove(perform:) API reorderable ใหม่ ทำงานกับ container ใดก็ได้ รวมถึง LazyVStack และ 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 จัดการ drag preview, placeholder การแทรก และ animation การวาง อ็อบเจกต์ ReorderDifference ให้ ID ต้นทางและปลายทาง โดยปล่อยให้การอัปเดตโมเดลเป็นของโค้ดแอปพลิเคชัน

เซกชันและส่วนหัวที่ปรับแต่งแล้ว

การจัดเนื้อหาเป็นเซกชันช่วยปรับปรุงความสามารถในการอ่านแต่อาจส่งผลกระทบต่อประสิทธิภาพหากใช้งานไม่ดี ส่วนหัวที่ตรึงและการจัดการเซกชันต้องการความใส่ใจเป็นพิเศษ

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

ส่วนหัวที่ตรึง (pinnedViews: [.sectionHeaders]) ยังคงมองเห็นได้ระหว่างการเลื่อน ปรับปรุงการนำทางในรายการที่ยาว

การแบ่งหน้าและการเลื่อนแบบไม่จำกัด

สำหรับชุดข้อมูลขนาดใหญ่ การแบ่งหน้าหลีกเลี่ยงการโหลดข้อมูลทั้งหมดลงในหน่วยความจำ การใช้งานต้องโปร่งใสสำหรับผู้ใช้

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

รูปแบบนี้รับประกันการโหลดที่ลื่นไหลโดยไม่ปิดกั้นอินเทอร์เฟซและช่วยให้สามารถจัดการหน่วยความจำได้อย่างมีประสิทธิภาพ

พร้อมที่จะพิชิตการสัมภาษณ์ iOS แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

การทำ Profiling ด้วย Instruments

การระบุปัญหาด้านประสิทธิภาพต้องการเครื่องมือวัดผลที่แม่นยำ Instruments มีเทมเพลตหลายแบบที่เหมาะสำหรับ 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
            }
    }
}

รายการตรวจสอบการปรับแต่งด้วย Instruments

สำหรับการทำ profiling รายการ SwiftUI ที่มีประสิทธิภาพ:

  1. Time Profiler: ระบุฟังก์ชันที่ใช้ CPU มากที่สุด
  2. Allocations: ตรวจสอบการเติบโตของหน่วยความจำระหว่างการเลื่อน
  3. SwiftUI Instrument: แสดงภาพการประเมิน body
  4. Core Animation: ตรวจจับการลดลงของเฟรม
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()

API debugging ของ SwiftUI นี้จะพิมพ์ไปยังคอนโซลว่ามีคุณสมบัติใดที่เปลี่ยนแปลงและกระตุ้นการประเมิน body ใหม่ จำเป็นสำหรับการระบุการเรนเดอร์ใหม่ที่ไม่จำเป็น

การปรับแต่งขั้นสูงด้วย drawingGroup

สำหรับวิวที่ซับซ้อนซึ่งมีเอฟเฟกต์ภาพหลายอย่าง drawingGroup() สามารถปรับปรุงประสิทธิภาพอย่างมีนัยสำคัญโดยการแรสเตอร์ไรซ์วิวเป็นเลเยอร์ 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() มีประสิทธิภาพเป็นพิเศษสำหรับวิวที่ผสมผสานเกรเดียนต์ เงา และเอฟเฟกต์เบลอ

แหล่งข้อมูล

  • WWDC26 SwiftUI Guide - เอกสารอย่างเป็นทางการของ Apple เกี่ยวกับการอัปเดต SwiftUI iOS 27 รวมถึงการแคช AsyncImage และ API reorderable
  • New SwiftUI APIs for Reordering and Drag and Drop on iOS 27 - คำอธิบายโดยละเอียดเกี่ยวกับ modifier .reorderable() และ .reorderContainer(for:)
  • What's New in SwiftUI for iOS 27 - ภาพรวมที่ครอบคลุมของการเปลี่ยนแปลง SwiftUI WWDC26

บทสรุป

การปรับแต่งรายการ SwiftUI ขึ้นอยู่กับความเข้าใจอย่างลึกซึ้งเกี่ยวกับกลไก lazy loading การรีไซเคิล และการเปรียบเทียบวิว iOS 27 นำการปรับปรุงที่สำคัญมาให้พร้อมกับการแคช AsyncImage native และ API swipe actions และ reordering ที่ไม่ขึ้นกับ container

รายการตรวจสอบประสิทธิภาพ SwiftUI

  • ใช้ LazyVStack หรือ List แทน VStack สำหรับคอลเลกชัน
  • ใช้ Identifiable ด้วย ID ที่เสถียรและไม่ซ้ำกัน
  • นำ Equatable มาใช้สำหรับเซลล์ที่ซับซ้อน
  • ใช้ประโยชน์จากการแคช AsyncImage native ของ iOS 27 หรือใช้การแคชแบบกำหนดเองสำหรับเวอร์ชันก่อนหน้า
  • โหลดข้อมูลล่วงหน้าด้วย prefetching อัจฉริยะ
  • เลือก List สำหรับการรีไซเคิลเซลล์หรือ LazyVStack สำหรับเลย์เอาต์แบบกำหนดเอง (ทั้งสองตอนนี้รองรับ swipe actions และ reordering)
  • ใช้ pinnedViews สำหรับส่วนหัวเซกชัน
  • ใช้การแบ่งหน้าสำหรับชุดข้อมูลขนาดใหญ่
  • ทำ profiling เป็นประจำด้วย Instruments
  • ใช้ drawingGroup() กับวิวที่มีเอฟเฟกต์ภาพที่ซับซ้อน

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

ชาเลนจ์ประจำวัน

คุณหาบั๊กใน iOS เจอไหม

โค้ดจริงหนึ่งชิ้น บั๊กที่ซ่อนอยู่หนึ่งจุด วันละหนึ่งครั้ง ลองได้โดยไม่ต้องมีบัญชี

Anthony Fillion-Maillet

เขียนโดย

Anthony Fillion-Maillet

ผู้ก่อตั้ง SharpSkill

เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่

อัปเดตเมื่อ 19 สิงหาคม 2569

แท็ก

#swiftui
#ios
#performance
#lazyvstack
#swift

แชร์

บทความที่เกี่ยวข้อง

งาน iOS Developer ปี 2026

งาน iOS Developer ปี 2026: แหล่งหางาน ช่วงเงินเดือน และการเตรียมตัวสัมภาษณ์

คู่มือครบถ้วนสำหรับการหางาน iOS Developer ในปี 2026 ค้นพบแหล่งหางานที่ดีที่สุด ข้อมูลเงินเดือนล่าสุด และกลยุทธ์การเตรียมตัวสัมภาษณ์เทคนิค

ViewModifier ของ SwiftUI สำหรับสร้าง design system ที่นำกลับมาใช้ใหม่ได้ใน iOS

ViewModifier แบบกำหนดเองใน SwiftUI: รูปแบบที่นำกลับมาใช้ใหม่ได้สำหรับ Design System

สร้าง ViewModifier แบบกำหนดเองใน SwiftUI สำหรับ design system ที่สอดคล้องกัน รูปแบบ แนวทางปฏิบัติที่ดีที่สุด และตัวอย่างที่ใช้งานได้จริงสำหรับการจัดสไตล์ view ของ iOS อย่างมีประสิทธิภาพ

การเปรียบเทียบ @Observable และ @State ใน SwiftUI สำหรับนักพัฒนา iOS

SwiftUI @Observable vs @State: ใช้ตัวไหนเมื่อไหร่ในปี 2026

เข้าใจความแตกต่างระหว่าง @Observable และ @State ใน SwiftUI เพื่อเลือกเครื่องมือจัดการ state ที่เหมาะกับแอป iOS