SwiftUI Performance: LazyVStack en Complexe Lijsten Optimaliseren
Optimalisatietechnieken voor LazyVStack en SwiftUI-lijsten. Verminder geheugengebruik, verbeter scrollprestaties en vermijd veelvoorkomende valkuilen.

Lijsten behoren tot de meest gebruikte componenten in iOS-applicaties. SwiftUI's LazyVStack en List bieden performante oplossingen voor het tonen van datacollecties, maar onjuist gebruik kan de gebruikerservaring snel verslechteren. Het begrijpen van de interne werking van deze componenten helpt veelvoorkomende valkuilen te vermijden en soepele interfaces te bouwen.
Dit artikel presenteert de essentiële optimalisatietechnieken voor SwiftUI-lijsten: lazy loading, view-recycling, identifierbeheer en geavanceerde patronen voor grote datasets. Bijgewerkt voor iOS 27 met de nieuwe reorder- en swipe action-API's.
Lazy Loading in SwiftUI Begrijpen
Lazy loading instantieert views pas wanneer ze zichtbaar worden op het scherm. In tegenstelling tot VStack, dat alle child-views direct creëert, stelt LazyVStack deze creatie uit, wat het geheugengebruik en de initiële rendertijd drastisch vermindert.
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)
}
}
}
}
}Het performanceverschil wordt al bij enkele honderden elementen significant. Met 10.000 items kan VStack enkele seconden nodig hebben om op te starten, terwijl LazyVStack direct reageert.
De Impact van Lazy Loading Meten
Met Instruments kan het geheugen- en CPU-gebruik nauwkeurig worden gemeten. Hier is een testview die het verschil illustreert:
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()
}
}Bij uitvoering met VStack verschijnen alle 10.000 logs meteen. Met LazyVStack worden alleen de zichtbare elementen (ongeveer 15-20 afhankelijk van de schermgrootte) initieel gelogd, terwijl meer verschijnen tijdens het scrollen.
LazyVStack houdt aangemaakte views in geheugen nadat ze verschijnen. In tegenstelling tot List, dat cellen actief recycled, blijven views in een LazyVStack bestaan totdat het ouder-component wordt vernietigd.
Het Belang van Stabiele Identifiers
Identifiers vormen het centrale mechanisme voor SwiftUI-lijstupdates. Een onstabiele identifier veroorzaakt onnodige view-hercreaties en kan visuele bugs veroorzaken zoals onjuiste animaties of verlies van de scrollpositie.
// ❌ 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)
}
}
}
}Het gebruik van een unieke en persistente identifier zorgt ervoor dat SwiftUI elementen correct kan onderscheiden bij updates, animaties en vergelijkingen.
Celoptimalisatie met Equatable
SwiftUI vergelijkt views om te bepalen of een nieuwe rendering nodig is. Standaard gebruikt deze vergelijking reflection, wat duur kan zijn. Het implementeren van Equatable maakt een geoptimaliseerde en expliciete vergelijking mogelijk.
// 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))
}
}
}
}Deze optimalisatie vermindert de CPU-belasting tijdens snel scrollen aanzienlijk, vooral bij cellen met berekeningen of afbeeldingen.
Klaar om je iOS gesprekken te halen?
Oefen met onze interactieve simulatoren, flashcards en technische tests.
Asynchroon Laden van Afbeeldingen Beheren
Afbeeldingen worden vaak het performance-knelpunt in lijsten. iOS 27 introduceerde standaard HTTP-caching voor AsyncImage als standaard, waardoor handmatige caching in veel scenario's overbodig wordt. Voor applicaties die iOS 26 en eerder ondersteunen, of die fijnmazige controle over caching-gedrag vereisen, blijft een aangepaste implementatie waardevol.
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)
}
}
}
}Voor iOS 27 en later respecteert standaard AsyncImage automatisch HTTP cache headers. Een aangepaste URLCache kan worden toegepast via de asyncImageURLSession(_:) modifier voor specifieke vereisten.
Slim Prefetching voor Anticipatie
Voor zeer lange lijsten laadt prefetching afbeeldingen voordat ze zichtbaar worden:
// 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 { $0.avatarURL }
)
for (url, task) in prefetchTasks {
if !validURLs.contains(url) {
task.cancel()
prefetchTasks.removeValue(forKey: url)
}
}
}
}Kiezen tussen List en LazyVStack
iOS 27 heeft het verschil tussen List en LazyVStack aanzienlijk verkleind. Voorheen was List de enige optie voor native swipe actions en drag-to-reorder. De nieuwe .swipeActionsContainer() en .reorderable() API's brengen deze mogelijkheden naar elke 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 { $0.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 { $0.id == post.id }
}
}De keuze hangt nu af van visuele vereisten in plaats van beschikbare functies:
| Vereiste | Aanbevolen Container |
|---|---|
| Cel-recycling voor geheugenefficiëntie | List |
| Aangepaste spacing en padding | LazyVStack |
| Ingebouwde scheidingslijnen en insets | List |
| Kaart-gebaseerde of niet-standaard layouts | LazyVStack |
| Sectie-headers met sticky-gedrag | Beide (gebruik pinnedViews) |
| Swipe actions | Beide (iOS 27+) |
| Drag-to-reorder | Beide (iOS 27+) |
List recycled cellen actief, wat problemen kan veroorzaken met lokale state (@State). @State-waarden in List-cellen kunnen onverwacht hergebruikt worden. Het is beter om state op te slaan in het datamodel of in een ViewModel.
Elementen Herschikken in LazyVStack (iOS 27)
Voor iOS 27 vereiste drag-to-reorder List met onMove(perform:). De nieuwe reorderable API's werken met elke container, inclusief LazyVStack en 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 { $0.id == id }
}
// Remove from original positions
self.removeAll { item in
difference.sources.contains(item.id)
}
// Insert at destination
if let destinationIndex = self.firstIndex(where: { $0.id == difference.destination }) {
self.insert(contentsOf: movedItems, at: destinationIndex)
} else {
self.append(contentsOf: movedItems)
}
}
}SwiftUI verzorgt de drag-preview, invoegplaatsaanduiding en drop-animatie. Het ReorderDifference-object levert de bron-ID's en bestemming, waarbij model-updates aan de applicatiecode worden overgelaten.
Geoptimaliseerde Secties en Headers
Het organiseren van content in secties verbetert de leesbaarheid maar kan de performance beïnvloeden bij slechte implementatie. Pinned headers en sectiebeheer vereisen bijzondere aandacht.
// 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: $0.key, contacts: $0.value) }
.sorted { $0.letter < $1.letter }
}
}Pinned headers (pinnedViews: [.sectionHeaders]) blijven zichtbaar tijdens het scrollen, wat de navigatie in lange lijsten verbetert.
Paginering en Oneindig Scrollen
Voor grote datasets voorkomt paginering dat alle data in het geheugen wordt geladen. De implementatie moet transparant zijn voor de gebruiker.
// 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: { $0.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]
}Dit patroon zorgt voor soepel laden zonder de interface te blokkeren en maakt efficiënt geheugenbeheer mogelijk.
Klaar om je iOS gesprekken te halen?
Oefen met onze interactieve simulatoren, flashcards en technische tests.
Profilen met Instruments
Het identificeren van performanceproblemen vereist nauwkeurige meetinstrumenten. Instruments biedt verschillende templates die geschikt zijn voor 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-optimalisatiechecklist
Voor effectieve profiling van SwiftUI-lijsten:
- Time Profiler: identificeer functies die de meeste CPU verbruiken
- Allocations: controleer geheugengroei tijdens scrollen
- SwiftUI Instrument: visualiseer body-evaluaties
- Core Animation: detecteer frame drops
// 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)")
}
}
}
}Deze SwiftUI-debugging-API print naar de console welke eigenschappen zijn gewijzigd en een herevaluatie van body hebben veroorzaakt. Essentieel voor het identificeren van onnodige renderings.
Geavanceerde Optimalisaties met drawingGroup
Voor complexe views met veel visuele effecten kan drawingGroup() de performance aanzienlijk verbeteren door de view te rasteriseren naar een Metal-laag.
// 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() is bijzonder effectief voor views die gradiënten, schaduwen en blur-effecten combineren.
Bronnen
- WWDC26 SwiftUI Guide - Officiële Apple-documentatie over iOS 27 SwiftUI-updates inclusief AsyncImage caching en reorderable API's
- New SwiftUI APIs for Reordering and Drag and Drop on iOS 27 - Gedetailleerde uitleg van
.reorderable()en.reorderContainer(for:)modifiers - What's New in SwiftUI for iOS 27 - Uitgebreid overzicht van WWDC26 SwiftUI-wijzigingen
Conclusie
De optimalisatie van SwiftUI-lijsten berust op een diep begrip van de mechanismen voor lazy loading, recycling en view-vergelijking. iOS 27 bracht significante verbeteringen met native AsyncImage caching en container-agnostische swipe actions en reorder API's.
SwiftUI Performance Checklist
- Gebruik
LazyVStackofListin plaats vanVStackvoor collecties - Implementeer
Identifiablemet stabiele, unieke ID's - Adopteer
Equatablevoor complexe cellen - Benut iOS 27's native AsyncImage caching, of implementeer aangepaste caching voor eerdere versies
- Laad data vooraf met slim prefetching
- Kies
Listvoor cel-recycling ofLazyVStackvoor aangepaste layouts (beide ondersteunen nu swipe actions en herschikking) - Gebruik
pinnedViewsvoor sectie-headers - Implementeer paginering voor grote datasets
- Profiel regelmatig met Instruments
- Pas
drawingGroup()toe op views met complexe visuele effecten
Begin met oefenen!
Test je kennis met onze gespreksimulatoren en technische tests.
Zie jij de bug in iOS?
Een echt codefragment, een verborgen bug, één poging per dag. Zonder account uit te proberen.

Geschreven door
Anthony Fillion-MailletOprichter van SharpSkill
Al meer dan 10 jaar fullstack-ontwikkelaar. Hij leidt SharpSkill en staat in voor alles wat hier verschijnt.
Bijgewerkt op 19 augustus 2026
Tags
Delen
Gerelateerde artikelen

iOS-ontwikkelaar vacatures 2026: waar zoeken, salarissen en sollicitatievoorbereidingen
De arbeidsmarkt voor iOS-ontwikkelaars in 2026: waar vacatures te vinden zijn, actuele salarisbereiken en hoe je je optimaal voorbereidt op technische sollicitatiegesprekken.

Custom SwiftUI ViewModifiers: herbruikbare patterns voor design systems
Bouw custom ViewModifiers in SwiftUI voor een consistent design system. Patterns, best practices en praktische voorbeelden om iOS-views efficiënt te stijlen.

SwiftUI @Observable vs @State: Wanneer Wat Gebruiken in 2026
Beheers de verschillen tussen @Observable en @State in SwiftUI om de juiste tool voor state management in iOS-apps te kiezen.