SwiftUI Performance : optimiser LazyVStack et listes complexes
Techniques d'optimisation pour LazyVStack et listes SwiftUI. Réduire la consommation mémoire, améliorer le scrolling et éviter les pièges de performance courants.

Les listes représentent l'un des composants les plus utilisés dans les applications iOS. Avec SwiftUI, LazyVStack et List offrent des solutions performantes pour afficher des collections de données, mais leur utilisation incorrecte peut rapidement dégrader l'expérience utilisateur. Comprendre les mécanismes internes de ces composants permet d'éviter les pièges courants et de construire des interfaces fluides.
Cet article présente les techniques d'optimisation essentielles pour les listes SwiftUI : lazy loading, recyclage des vues, gestion des identifiants et patterns avancés pour les données volumineuses. Mis à jour pour iOS 27 avec les nouvelles APIs de réordonnancement et d'actions de balayage.
Comprendre le lazy loading en SwiftUI
Le principe du lazy loading repose sur l'instanciation des vues uniquement lorsqu'elles deviennent visibles à l'écran. Contrairement à VStack qui crée immédiatement toutes ses vues enfants, LazyVStack diffère cette création, réduisant drastiquement la consommation mémoire et le temps de rendu initial.
import SwiftUI
// ❌ Problem: VStack instantiates all 10,000 views immediately
struct NonLazyListView: View {
let items = (1...10000).map { "Item (\$0)" }
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 (\$0)" }
var body: some View {
ScrollView {
LazyVStack {
ForEach(items, id: \.self) { item in
// Only visible views are created
ExpensiveRowView(title: item)
}
}
}
}
}La différence de performance devient significative dès quelques centaines d'éléments. Avec 10 000 items, VStack peut prendre plusieurs secondes au lancement tandis que LazyVStack reste instantané.
Mesurer l'impact du lazy loading
Instruments permet de mesurer précisément l'utilisation mémoire et CPU. Voici une vue de test qui illustre la différence :
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()
}
}En exécutant avec VStack, les 10 000 logs apparaissent immédiatement. Avec LazyVStack, seuls les éléments visibles (environ 15-20 selon la taille de l'écran) sont enregistrés au démarrage, et d'autres apparaissent au fur et à mesure du scrolling.
LazyVStack conserve les vues créées en mémoire après leur apparition. Contrairement à List qui recycle activement les cellules, les vues dans un LazyVStack persistent jusqu'à la destruction du composant parent.
L'importance des identifiants stables
Les identifiants constituent le mécanisme central des mises à jour de listes SwiftUI. Un identifiant instable provoque des recréations de vues inutiles et peut générer des bugs visuels comme des animations incorrectes ou une perte de position de 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)
}
}
}
}L'utilisation d'un identifiant unique et persistant garantit que SwiftUI peut correctement différencier les éléments lors des mises à jour, des animations et des comparaisons.
Optimisation des cellules avec Equatable
SwiftUI compare les vues pour déterminer si un nouveau rendu est nécessaire. Par défaut, cette comparaison utilise la réflexion, ce qui peut être coûteux. Implémenter Equatable permet une comparaison optimisée et explicite.
// 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))
}
}
}
}Cette optimisation réduit significativement la charge CPU lors du scrolling rapide, particulièrement avec des cellules contenant des calculs ou des images.
Prêt à réussir tes entretiens iOS ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
Gérer le chargement asynchrone des images
Les images constituent souvent le goulot d'étranglement de performance des listes. iOS 27 a introduit le cache HTTP standard pour AsyncImage par défaut, éliminant le besoin de cache manuel dans de nombreux scénarios. Pour les applications ciblant iOS 26 ou versions antérieures, ou nécessitant un contrôle fin sur le comportement du cache, une implémentation personnalisée reste pertinente.
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)
}
}
}
}Pour iOS 27 et versions ultérieures, AsyncImage standard respecte automatiquement les headers de cache HTTP. Un URLCache personnalisé peut être appliqué via le modificateur asyncImageURLSession(_:) pour des besoins spécifiques.
Prefetching intelligent pour anticiper
Pour les listes très longues, le prefetching charge les images avant qu'elles ne deviennent visibles :
// 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)
}
}
}
}Choisir entre List et LazyVStack
iOS 27 a considérablement réduit l'écart entre List et LazyVStack. Auparavant, List était la seule option pour les actions de balayage natives et le glisser-déposer pour réordonner. Les nouvelles APIs .swipeActionsContainer() et .reorderable() apportent ces capacités à tout conteneur.
// ✅ 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 }
}
}Le choix dépend désormais des exigences visuelles plutôt que de la disponibilité des fonctionnalités :
| Exigence | Conteneur recommandé |
|---|---|
| Recyclage des cellules pour l'efficacité mémoire | List |
| Espacement et padding personnalisés | LazyVStack |
| Séparateurs et insets natifs | List |
| Layouts type carte ou non standard | LazyVStack |
| Headers de section avec comportement sticky | Les deux (utiliser pinnedViews) |
| Actions de balayage | Les deux (iOS 27+) |
| Glisser-déposer pour réordonner | Les deux (iOS 27+) |
List recycle activement les cellules, ce qui peut causer des problèmes avec l'état local (@State). Les valeurs @State dans les cellules de List peuvent être réutilisées de manière inattendue. Il convient de stocker l'état dans le modèle de données ou dans un ViewModel.
Réordonner les éléments dans LazyVStack (iOS 27)
Avant iOS 27, le glisser-déposer pour réordonner nécessitait List avec onMove(perform:). Les nouvelles APIs reorderable fonctionnent avec n'importe quel conteneur, y compris LazyVStack et 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 gère la prévisualisation du glissement, le marqueur d'insertion et l'animation de dépôt. L'objet ReorderDifference fournit les IDs source et la destination, laissant les mises à jour du modèle au code de l'application.
Sections et headers optimisés
Organiser le contenu en sections améliore la lisibilité mais peut impacter les performances si mal implémenté. Les headers épinglés et la gestion des sections nécessitent une attention particulière.
// 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 }
}
}Les headers épinglés (pinnedViews: [.sectionHeaders]) restent visibles pendant le scrolling, améliorant la navigation dans les listes longues.
Pagination et scroll infini
Pour les données volumineuses, la pagination évite de charger toutes les données en mémoire. L'implémentation doit être transparente pour l'utilisateur.
// 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]
}Ce pattern garantit un chargement fluide sans bloquer l'interface et permet une gestion efficace de la mémoire.
Prêt à réussir tes entretiens iOS ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
Profiling avec Instruments
Identifier les problèmes de performance nécessite des outils de mesure précis. Instruments propose plusieurs templates adaptés à 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 d'optimisation avec Instruments
Pour un profiling efficace des listes SwiftUI :
- Time Profiler : identifier les fonctions qui consomment le plus de CPU
- Allocations : vérifier la croissance mémoire pendant le scrolling
- SwiftUI Instrument : visualiser les évaluations de body
- Core Animation : détecter les chutes de frames
// 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)")
}
}
}
}Cette API de debugging SwiftUI affiche dans la console quelles propriétés ont changé et déclenché une réévaluation du body. Essentiel pour identifier les rendus inutiles.
Optimisations avancées avec drawingGroup
Pour les vues complexes avec de nombreux effets visuels, drawingGroup() peut améliorer significativement les performances en rastérisant la vue dans une couche 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() est particulièrement efficace pour les vues combinant dégradés, ombres et effets de flou.
Sources
- WWDC26 SwiftUI Guide - Documentation officielle Apple sur les mises à jour SwiftUI iOS 27 incluant le cache AsyncImage et les APIs reorderable
- New SwiftUI APIs for Reordering and Drag and Drop on iOS 27 - Explication détaillée des modificateurs
.reorderable()et.reorderContainer(for:) - What's New in SwiftUI for iOS 27 - Vue d'ensemble complète des changements SwiftUI de WWDC26
Conclusion
L'optimisation des listes SwiftUI repose sur une compréhension approfondie des mécanismes de lazy loading, de recyclage et de comparaison des vues. iOS 27 a apporté des améliorations significatives avec le cache natif AsyncImage et les APIs d'actions de balayage et de réordonnancement indépendantes du conteneur.
Checklist de performance SwiftUI
- Utiliser
LazyVStackouListau lieu deVStackpour les collections - Implémenter
Identifiableavec des IDs stables et uniques - Adopter
Equatablepour les cellules complexes - Exploiter le cache natif AsyncImage d'iOS 27, ou implémenter un cache personnalisé pour les versions antérieures
- Précharger les données avec un prefetching intelligent
- Choisir
Listpour le recyclage des cellules ouLazyVStackpour les layouts personnalisés (les deux supportent désormais les actions de balayage et le réordonnancement) - Utiliser
pinnedViewspour les headers de section - Implémenter la pagination pour les données volumineuses
- Faire du profiling régulier avec Instruments
- Appliquer
drawingGroup()aux vues avec des effets visuels complexes
Passe à la pratique !
Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.
Tu saurais repérer le bug en iOS ?
Un vrai bout de code, un bug caché, une tentative par jour. Sans compte pour essayer.

Écrit par
Anthony Fillion-MailletFondateur de SharpSkill
Développeur fullstack depuis plus de 10 ans. Il dirige SharpSkill et répond de tout ce qui y est publié.
Mis à jour le 19 août 2026
Tags
Partager
Articles similaires

SwiftUI Custom ViewModifiers : patterns réutilisables pour design system
Créer des ViewModifiers SwiftUI personnalisés pour un design system cohérent. Patterns, bonnes pratiques et exemples concrets pour styliser vos vues iOS efficacement.

SwiftUI @Observable vs @State : quand utiliser quoi en 2026
Comprendre les différences entre @Observable et @State en SwiftUI pour choisir le bon outil de gestion d'état selon le contexte de votre application iOS.

CloudKit avec SwiftUI en 2026 : synchronisation de données cross-device
Guide complet pour implémenter la synchronisation CloudKit avec SwiftUI : CKSyncEngine, intégration SwiftData, gestion des conflits et bonnes pratiques iOS 2026.