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

รายการเป็นหนึ่งในส่วนประกอบที่ใช้บ่อยที่สุดในแอปพลิเคชัน iOS LazyVStack และ List ของ SwiftUI ให้โซลูชันที่มีประสิทธิภาพในการแสดงคอลเลกชันข้อมูล แต่การใช้งานที่ไม่ถูกต้องอาจลดประสบการณ์ผู้ใช้ได้อย่างรวดเร็ว การเข้าใจกลไกภายในของส่วนประกอบเหล่านี้ช่วยหลีกเลี่ยงข้อผิดพลาดที่พบบ่อยและสร้างอินเทอร์เฟซที่ลื่นไหล
บทความนี้นำเสนอเทคนิคการปรับแต่งที่จำเป็นสำหรับรายการ SwiftUI: lazy loading การรีไซเคิลวิว การจัดการตัวระบุ และรูปแบบขั้นสูงสำหรับชุดข้อมูลขนาดใหญ่ อัปเดตสำหรับ iOS 27 พร้อม API การจัดเรียงใหม่และ swipe action ใหม่
ทำความเข้าใจ Lazy Loading ใน SwiftUI
Lazy loading จะสร้างวิวเฉพาะเมื่อปรากฏบนหน้าจอเท่านั้น ต่างจาก VStack ที่สร้างวิวลูกทั้งหมดทันที LazyVStack จะเลื่อนการสร้างนี้ออกไป ลดการใช้หน่วยความจำและเวลาในการเรนเดอร์เริ่มต้นได้อย่างมาก
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 ได้อย่างแม่นยำ นี่คือวิวทดสอบที่แสดงความแตกต่าง:
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 ตัวระบุที่ไม่เสถียรทำให้เกิดการสร้างวิวใหม่ที่ไม่จำเป็นและอาจกระตุ้นบั๊กทางสายตา เช่น แอนิเมชันที่ผิดพลาดหรือการสูญเสียตำแหน่งการเลื่อน
// ❌ 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 ช่วยให้สามารถเปรียบเทียบได้อย่างมีประสิทธิภาพและชัดเจน
// 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 และก่อนหน้า หรือต้องการการควบคุมการแคชอย่างละเอียด การใช้งานแบบกำหนดเองยังคงมีคุณค่า
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 จะโหลดรูปภาพก่อนที่จะปรากฏ:
// 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 ใดก็ได้
// ✅ 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
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 ต้นทางและปลายทาง โดยปล่อยให้การอัปเดตโมเดลเป็นของโค้ดแอปพลิเคชัน
เซกชันและส่วนหัวที่ปรับแต่งแล้ว
การจัดเนื้อหาเป็นเซกชันช่วยปรับปรุงความสามารถในการอ่านแต่อาจส่งผลกระทบต่อประสิทธิภาพหากใช้งานไม่ดี ส่วนหัวที่ตรึงและการจัดการเซกชันต้องการความใส่ใจเป็นพิเศษ
// 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]) ยังคงมองเห็นได้ระหว่างการเลื่อน ปรับปรุงการนำทางในรายการที่ยาว
การแบ่งหน้าและการเลื่อนแบบไม่จำกัด
สำหรับชุดข้อมูลขนาดใหญ่ การแบ่งหน้าหลีกเลี่ยงการโหลดข้อมูลทั้งหมดลงในหน่วยความจำ การใช้งานต้องโปร่งใสสำหรับผู้ใช้
// 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
// 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 ที่มีประสิทธิภาพ:
- Time Profiler: ระบุฟังก์ชันที่ใช้ CPU มากที่สุด
- Allocations: ตรวจสอบการเติบโตของหน่วยความจำระหว่างการเลื่อน
- SwiftUI Instrument: แสดงภาพการประเมิน body
- Core Animation: ตรวจจับการลดลงของเฟรม
// 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)")
}
}
}
}API debugging ของ SwiftUI นี้จะพิมพ์ไปยังคอนโซลว่ามีคุณสมบัติใดที่เปลี่ยนแปลงและกระตุ้นการประเมิน body ใหม่ จำเป็นสำหรับการระบุการเรนเดอร์ใหม่ที่ไม่จำเป็น
การปรับแต่งขั้นสูงด้วย drawingGroup
สำหรับวิวที่ซับซ้อนซึ่งมีเอฟเฟกต์ภาพหลายอย่าง drawingGroup() สามารถปรับปรุงประสิทธิภาพอย่างมีนัยสำคัญโดยการแรสเตอร์ไรซ์วิวเป็นเลเยอร์ Metal
// 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ผู้ก่อตั้ง SharpSkill
เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่
อัปเดตเมื่อ 19 สิงหาคม 2569
แท็ก
แชร์
บทความที่เกี่ยวข้อง

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

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

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