SwiftUI-Performance: LazyVStack und komplexe Listen optimieren
Optimierungstechniken für LazyVStack und SwiftUI-Listen. Speicherverbrauch reduzieren, Scroll-Performance verbessern und häufige Fallstricke vermeiden.

Listen gehören zu den am häufigsten verwendeten Komponenten in iOS-Anwendungen. SwiftUIs LazyVStack und List bieten performante Lösungen zur Anzeige von Datensammlungen, doch eine falsche Verwendung kann die Benutzererfahrung schnell beeinträchtigen. Das Verständnis der internen Mechanik dieser Komponenten hilft dabei, häufige Fallstricke zu vermeiden und flüssige Oberflächen zu bauen.
Dieser Artikel stellt die wichtigsten Optimierungstechniken für SwiftUI-Listen vor: Lazy Loading, View-Recycling, Identifier-Verwaltung und fortgeschrittene Muster für große Datensätze. Aktualisiert für iOS 27 mit den neuen Reorder- und Swipe-Action-APIs.
Lazy Loading in SwiftUI verstehen
Lazy Loading instanziiert Views erst dann, wenn sie auf dem Bildschirm sichtbar werden. Anders als VStack, das alle Kind-Views sofort erstellt, verzögert LazyVStack diese Erstellung und reduziert so drastisch den Speicherverbrauch und die initiale Renderzeit.
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)
}
}
}
}
}Der Performance-Unterschied wird bereits bei wenigen Hundert Elementen deutlich. Bei 10.000 Items kann VStack mehrere Sekunden zum Starten benötigen, während LazyVStack sofort reagiert.
Die Auswirkung von Lazy Loading messen
Instruments ermöglicht eine präzise Messung von Speicher- und CPU-Auslastung. Hier eine Test-View, die den Unterschied veranschaulicht:
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()
}
}Bei der Ausführung mit VStack erscheinen alle 10.000 Logs sofort. Mit LazyVStack werden nur die sichtbaren Elemente (etwa 15-20 je nach Bildschirmgröße) initial geloggt, weitere kommen beim Scrollen hinzu.
LazyVStack behält erstellte Views nach ihrem Erscheinen im Speicher. Anders als List, das Zellen aktiv wiederverwendet, bleiben Views in einem LazyVStack bestehen, bis die übergeordnete Komponente zerstört wird.
Die Bedeutung stabiler Identifier
Identifier bilden den zentralen Mechanismus für SwiftUI-Listen-Updates. Ein instabiler Identifier verursacht unnötige View-Neuerstellungen und kann visuelle Bugs wie falsche Animationen oder den Verlust der Scroll-Position auslösen.
// ❌ 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)
}
}
}
}Ein eindeutiger und persistenter Identifier stellt sicher, dass SwiftUI Elemente bei Updates, Animationen und Vergleichen korrekt unterscheiden kann.
Zellenoptimierung mit Equatable
SwiftUI vergleicht Views, um zu entscheiden, ob ein erneutes Rendering nötig ist. Standardmäßig nutzt dieser Vergleich Reflection, was teuer sein kann. Die Implementierung von Equatable ermöglicht einen optimierten und expliziten Vergleich.
// 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))
}
}
}
}Diese Optimierung reduziert die CPU-Last beim schnellen Scrollen erheblich, besonders bei Zellen mit Berechnungen oder Bildern.
Bereit für deine iOS-Interviews?
Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.
Asynchrones Laden von Bildern verwalten
Bilder werden in Listen häufig zum Performance-Bottleneck. iOS 27 führt standardmäßiges HTTP-Caching für AsyncImage ein, wodurch manuelles Caching in vielen Szenarien überflüssig wird. Für Anwendungen, die iOS 26 und früher unterstützen oder eine feinere Kontrolle über das Caching-Verhalten benötigen, bleibt eine benutzerdefinierte Implementierung wertvoll.
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)
}
}
}
}Für iOS 27 und höher respektiert das Standard-AsyncImage automatisch HTTP-Cache-Header. Eine benutzerdefinierte URLCache kann über den asyncImageURLSession(_:)-Modifier für spezifische Anforderungen angewendet werden.
Intelligentes Prefetching zur Vorhersage
Für sehr lange Listen lädt Prefetching Bilder, bevor sie sichtbar werden:
// 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)
}
}
}
}Zwischen List und LazyVStack wählen
iOS 27 hat den Unterschied zwischen List und LazyVStack deutlich verringert. Zuvor war List die einzige Option für native Swipe-Actions und Drag-to-Reorder. Die neuen .swipeActionsContainer() und .reorderable()-APIs bringen diese Funktionen in jeden 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 }
}
}Die Wahl hängt nun von visuellen Anforderungen ab, nicht von verfügbaren Funktionen:
| Anforderung | Empfohlener Container |
|---|---|
| Cell-Recycling für Speichereffizienz | List |
| Benutzerdefinierte Abstände und Padding | LazyVStack |
| Eingebaute Separatoren und Insets | List |
| Karten-basierte oder nicht-standardmäßige Layouts | LazyVStack |
| Section-Header mit Sticky-Verhalten | Beide (mit pinnedViews) |
| Swipe-Actions | Beide (iOS 27+) |
| Drag-to-Reorder | Beide (iOS 27+) |
List recycelt Zellen aktiv, was zu Problemen mit lokalem State (@State) führen kann. @State-Werte in List-Zellen können unerwartet wiederverwendet werden. State sollte besser im Datenmodell oder in einem ViewModel gespeichert werden.
Elemente in LazyVStack neu anordnen (iOS 27)
Vor iOS 27 erforderte Drag-to-Reorder List mit onMove(perform:). Die neuen Reorderable-APIs funktionieren mit jedem Container, einschließlich LazyVStack und 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 übernimmt die Drag-Preview, den Einfüge-Platzhalter und die Drop-Animation. Das ReorderDifference-Objekt liefert die Quell-IDs und das Ziel, wobei Model-Updates dem Anwendungscode überlassen bleiben.
Optimierte Sektionen und Header
Das Organisieren von Inhalten in Sektionen verbessert die Lesbarkeit, kann aber bei schlechter Implementierung die Performance beeinträchtigen. Pinned Headers und Sektionsverwaltung erfordern besondere Aufmerksamkeit.
// 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]) bleiben beim Scrollen sichtbar und verbessern die Navigation in langen Listen.
Pagination und unendliches Scrollen
Für große Datensätze vermeidet Pagination das Laden aller Daten in den Speicher. Die Implementierung sollte für den Benutzer transparent sein.
// 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]
}Dieses Muster gewährleistet ein flüssiges Laden ohne die Oberfläche zu blockieren und ermöglicht eine effiziente Speicherverwaltung.
Bereit für deine iOS-Interviews?
Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.
Profiling mit Instruments
Die Identifikation von Performance-Problemen erfordert präzise Messwerkzeuge. Instruments bietet mehrere für SwiftUI geeignete Templates.
// 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-Optimierungs-Checkliste
Für effektives Profiling von SwiftUI-Listen:
- Time Profiler: Funktionen mit der höchsten CPU-Last identifizieren
- Allocations: Speicherwachstum während des Scrollens überprüfen
- SwiftUI Instrument: Body-Auswertungen visualisieren
- Core Animation: Frame Drops erkennen
// 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)")
}
}
}
}Diese SwiftUI-Debugging-API gibt in der Konsole aus, welche Eigenschaften sich geändert und eine Body-Neuberechnung ausgelöst haben. Unverzichtbar zur Identifikation unnötiger Renderings.
Fortgeschrittene Optimierungen mit drawingGroup
Für komplexe Views mit vielen visuellen Effekten kann drawingGroup() die Performance deutlich verbessern, indem die View in einen Metal-Layer rasterisiert wird.
// 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() ist besonders effektiv bei Views, die Gradienten, Schatten und Blur-Effekte kombinieren.
Quellen
- WWDC26 SwiftUI Guide - Offizielle Apple-Dokumentation zu iOS 27 SwiftUI-Updates einschließlich AsyncImage-Caching und Reorderable-APIs
- New SwiftUI APIs for Reordering and Drag and Drop on iOS 27 - Detaillierte Erklärung der
.reorderable()und.reorderContainer(for:)-Modifier - What's New in SwiftUI for iOS 27 - Umfassender Überblick über WWDC26 SwiftUI-Änderungen
Fazit
Die Optimierung von SwiftUI-Listen beruht auf einem tiefen Verständnis der Mechanismen für Lazy Loading, Recycling und View-Vergleich. iOS 27 brachte bedeutende Verbesserungen mit nativem AsyncImage-Caching und Container-agnostischen Swipe-Actions sowie Reorder-APIs.
SwiftUI-Performance-Checkliste
LazyVStackoderListstattVStackfür Sammlungen verwendenIdentifiablemit stabilen, eindeutigen IDs implementierenEquatablefür komplexe Zellen einsetzen- iOS 27 natives AsyncImage-Caching nutzen oder benutzerdefiniertes Caching für frühere Versionen implementieren
- Daten mit intelligentem Prefetching vorab laden
Listfür Cell-Recycling oderLazyVStackfür eigene Layouts wählen (beide unterstützen jetzt Swipe-Actions und Reorder)pinnedViewsfür Sektions-Header nutzen- Pagination für große Datensätze implementieren
- Regelmäßig mit Instruments profilen
drawingGroup()auf Views mit komplexen visuellen Effekten anwenden
Fang an zu üben!
Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.
Findest du den Bug in iOS?
Ein echter Codeausschnitt, ein versteckter Bug, ein Versuch pro Tag. Zum Ausprobieren ohne Konto.

Geschrieben von
Anthony Fillion-MailletGründer von SharpSkill
Seit über 10 Jahren Fullstack-Entwickler. Er leitet SharpSkill und verantwortet alles, was hier erscheint.
Aktualisiert am 19. August 2026
Tags
Teilen
Verwandte Artikel

iOS-Entwickler Jobs 2026: Jobbörsen, Gehaltsübersicht und Interviewvorbereitung
Der Arbeitsmarkt für iOS-Entwickler 2026: Wo Stellenangebote zu finden sind, aktuelle Gehaltsspannen und wie sich Bewerber optimal auf technische Interviews vorbereiten.

Eigene SwiftUI-ViewModifier: wiederverwendbare Patterns für Design Systems
Eigene ViewModifier in SwiftUI für ein konsistentes Design System bauen. Patterns, Best Practices und praxisnahe Beispiele für effizientes iOS-View-Styling.

SwiftUI @Observable vs @State: Wann welches verwenden in 2026
Beherrsche die Unterschiede zwischen @Observable und @State in SwiftUI, um das richtige Tool für State-Management in iOS-Apps zu wählen.