Performa SwiftUI: Mengoptimalkan LazyVStack dan Daftar Kompleks
Teknik optimasi untuk LazyVStack dan daftar SwiftUI. Mengurangi konsumsi memori, meningkatkan performa scroll, dan menghindari kesalahan umum.

Daftar merupakan salah satu komponen yang paling sering digunakan dalam aplikasi iOS. LazyVStack dan List SwiftUI menyediakan solusi yang performant untuk menampilkan koleksi data, namun penggunaan yang salah dapat dengan cepat memperburuk pengalaman pengguna. Memahami mekanisme internal komponen-komponen ini membantu menghindari jebakan umum dan membangun antarmuka yang halus.
Artikel ini menyajikan teknik optimasi penting untuk daftar SwiftUI: lazy loading, daur ulang view, manajemen identifier, dan pola lanjutan untuk dataset besar. Diperbarui untuk iOS 27 dengan API reordering dan swipe action yang baru.
Memahami Lazy Loading di SwiftUI
Lazy loading menginstansiasi view hanya ketika view tersebut menjadi terlihat di layar. Berbeda dengan VStack yang segera membuat semua view anak, LazyVStack menunda pembuatan ini, sehingga secara drastis mengurangi konsumsi memori dan waktu render awal.
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)
}
}
}
}
}Perbedaan performa menjadi signifikan bahkan dengan beberapa ratus elemen saja. Dengan 10.000 item, VStack dapat memerlukan beberapa detik untuk diluncurkan sementara LazyVStack tetap instan.
Mengukur Dampak Lazy Loading
Instruments memungkinkan pengukuran penggunaan memori dan CPU secara presisi. Berikut ini adalah view tes yang mengilustrasikan perbedaannya:
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()
}
}Saat dijalankan dengan VStack, semua 10.000 log muncul secara langsung. Dengan LazyVStack, hanya elemen yang terlihat (sekitar 15-20 tergantung ukuran layar) yang awalnya dicatat, dengan lebih banyak yang muncul saat scroll dilakukan.
LazyVStack mempertahankan view yang sudah dibuat di memori setelah view tersebut muncul. Berbeda dengan List yang secara aktif mendaur ulang sel, view dalam LazyVStack bertahan hingga komponen induk dihancurkan.
Pentingnya Identifier yang Stabil
Identifier membentuk mekanisme sentral untuk pembaruan daftar SwiftUI. Identifier yang tidak stabil menyebabkan pembuatan ulang view yang tidak perlu dan dapat memicu bug visual seperti animasi yang salah atau kehilangan posisi scroll.
// ❌ 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)
}
}
}
}Menggunakan identifier yang unik dan persisten memastikan SwiftUI dapat membedakan elemen dengan benar selama pembaruan, animasi, dan perbandingan.
Optimasi Sel dengan Equatable
SwiftUI membandingkan view untuk menentukan apakah render ulang diperlukan. Secara default, perbandingan ini menggunakan reflection, yang dapat menjadi mahal. Mengimplementasikan Equatable memungkinkan perbandingan yang teroptimasi dan eksplisit.
// 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))
}
}
}
}Optimasi ini secara signifikan mengurangi beban CPU selama scroll cepat, terutama pada sel yang mengandung komputasi atau gambar.
Siap menguasai wawancara iOS Anda?
Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.
Mengelola Pemuatan Gambar Asinkron
Gambar sering kali menjadi bottleneck performa dalam daftar. iOS 27 memperkenalkan caching HTTP standar untuk AsyncImage secara default, menghilangkan kebutuhan caching manual dalam banyak skenario. Untuk aplikasi yang menargetkan iOS 26 dan sebelumnya, atau yang memerlukan kontrol caching yang lebih detail, implementasi kustom tetap berguna.
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)
}
}
}
}Untuk iOS 27 dan yang lebih baru, AsyncImage standar menghormati header cache HTTP secara otomatis. URLCache kustom dapat diterapkan melalui modifier asyncImageURLSession(_:) untuk kebutuhan khusus.
Prefetching Cerdas untuk Antisipasi
Untuk daftar yang sangat panjang, prefetching memuat gambar sebelum gambar tersebut menjadi terlihat:
// 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)
}
}
}
}Memilih antara List dan LazyVStack
iOS 27 secara signifikan mempersempit kesenjangan antara List dan LazyVStack. Sebelumnya, List adalah satu-satunya pilihan untuk swipe action native dan drag-to-reorder. API .swipeActionsContainer() dan .reorderable() yang baru membawa kemampuan ini ke container mana pun.
// ✅ 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 }
}
}Pilihan sekarang bergantung pada kebutuhan visual daripada ketersediaan fitur:
| Kebutuhan | Container yang Disarankan |
|---|---|
| Daur ulang sel untuk efisiensi memori | List |
| Spacing dan padding kustom | LazyVStack |
| Separator dan inset bawaan | List |
| Layout berbasis kartu atau non-standar | LazyVStack |
| Header section dengan perilaku sticky | Keduanya (gunakan pinnedViews) |
| Swipe actions | Keduanya (iOS 27+) |
| Drag-to-reorder | Keduanya (iOS 27+) |
List secara aktif mendaur ulang sel, yang dapat menyebabkan masalah dengan state lokal (@State). Nilai @State dalam sel List dapat digunakan kembali secara tak terduga. Sebaiknya simpan state dalam model data atau ViewModel.
Reordering Item di LazyVStack (iOS 27)
Sebelum iOS 27, drag-to-reorder memerlukan List dengan onMove(perform:). API reorderable yang baru berfungsi dengan container mana pun, termasuk LazyVStack dan 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 menangani drag preview, placeholder penyisipan, dan animasi drop. Objek ReorderDifference menyediakan ID sumber dan tujuan, menyerahkan pembaruan model ke kode aplikasi.
Section dan Header yang Teroptimasi
Mengorganisasi konten ke dalam section meningkatkan keterbacaan namun dapat memengaruhi performa jika diimplementasikan dengan buruk. Header yang dipinned dan manajemen section memerlukan perhatian khusus.
// 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 }
}
}Header yang dipinned (pinnedViews: [.sectionHeaders]) tetap terlihat selama scroll, meningkatkan navigasi pada daftar yang panjang.
Pagination dan Infinite Scrolling
Untuk dataset besar, pagination menghindari pemuatan semua data ke memori. Implementasi harus transparan bagi pengguna.
// 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]
}Pola ini memastikan pemuatan yang halus tanpa memblokir antarmuka dan memungkinkan manajemen memori yang efisien.
Siap menguasai wawancara iOS Anda?
Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.
Profiling dengan Instruments
Mengidentifikasi masalah performa membutuhkan alat pengukuran yang presisi. Instruments menawarkan beberapa template yang cocok untuk 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
}
}
}Checklist Optimasi dengan Instruments
Untuk profiling daftar SwiftUI yang efektif:
- Time Profiler: identifikasi fungsi yang paling banyak mengonsumsi CPU
- Allocations: verifikasi pertumbuhan memori selama scroll
- SwiftUI Instrument: visualisasikan evaluasi body
- Core Animation: deteksi penurunan frame
// 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 ini mencetak ke konsol properti mana yang berubah dan memicu evaluasi ulang body. Penting untuk mengidentifikasi render ulang yang tidak perlu.
Optimasi Lanjutan dengan drawingGroup
Untuk view kompleks dengan banyak efek visual, drawingGroup() dapat secara signifikan meningkatkan performa dengan merasterisasi view ke dalam lapisan 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() sangat efektif untuk view yang menggabungkan gradien, bayangan, dan efek blur.
Sumber
- WWDC26 SwiftUI Guide - Dokumentasi resmi Apple tentang pembaruan SwiftUI iOS 27 termasuk caching AsyncImage dan API reorderable
- New SwiftUI APIs for Reordering and Drag and Drop on iOS 27 - Penjelasan detail tentang modifier
.reorderable()dan.reorderContainer(for:) - What's New in SwiftUI for iOS 27 - Ringkasan komprehensif perubahan SwiftUI WWDC26
Kesimpulan
Optimasi daftar SwiftUI bergantung pada pemahaman mendalam tentang mekanisme lazy loading, daur ulang, dan perbandingan view. iOS 27 membawa peningkatan signifikan dengan caching AsyncImage native serta API swipe actions dan reordering yang container-agnostic.
Checklist Performa SwiftUI
- Gunakan
LazyVStackatauListalih-alihVStackuntuk koleksi - Implementasikan
Identifiabledengan ID yang stabil dan unik - Adopsi
Equatableuntuk sel yang kompleks - Manfaatkan caching AsyncImage native iOS 27, atau implementasikan caching kustom untuk versi sebelumnya
- Pra-muat data dengan prefetching cerdas
- Pilih
Listuntuk daur ulang sel atauLazyVStackuntuk layout kustom (keduanya kini mendukung swipe actions dan reordering) - Gunakan
pinnedViewsuntuk header section - Implementasikan pagination untuk dataset besar
- Lakukan profiling rutin dengan Instruments
- Terapkan
drawingGroup()pada view dengan efek visual kompleks
Mulai berlatih!
Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.
Bisakah kamu menemukan bug di iOS?
Satu potongan kode nyata, satu bug tersembunyi, satu percobaan per hari. Tanpa akun untuk mencoba.

Ditulis oleh
Anthony Fillion-MailletPendiri SharpSkill
Developer fullstack selama lebih dari 10 tahun. Ia menjalankan SharpSkill dan bertanggung jawab atas semua yang diterbitkan di sini.
Diperbarui 19 Agustus 2026
Tag
Bagikan
Artikel terkait

Lowongan Developer iOS 2026: Sumber Pencarian, Kisaran Gaji, dan Persiapan Interview
Panduan lengkap mencari pekerjaan developer iOS di tahun 2026. Temukan sumber lowongan terbaik, data gaji terkini, dan strategi persiapan interview teknis.

ViewModifier kustom di SwiftUI: pola yang dapat digunakan kembali untuk Design System
Bangun ViewModifier kustom di SwiftUI untuk design system yang konsisten. Pola, praktik terbaik, dan contoh praktis untuk menstilisasi view iOS secara efisien.

SwiftUI @Observable vs @State: Kapan Menggunakan yang Mana di 2026
Kuasai perbedaan antara @Observable dan @State di SwiftUI untuk memilih alat manajemen state yang tepat untuk aplikasi iOS Anda.