Bir iOS uygulamasında "neden bu kadar yavaş?" sorusunu hiç sordun mu? Kullanıcılar 100ms'den uzun süren yanıtları fark ediyor, 300ms üzerinde "yavaş" diyorlar. SwiftUI ile harika UI'lar yapmak kolay - ama performanslı yapmak bambaşka bir oyun. Bu rehberde, production uygulamalarında gerçekten fark yaratan optimizasyon tekniklerini derinlemesine inceleyeceğiz.
💡 Hızlı Not: Bu rehber Apple'ın WWDC23-24 session'ları, resmi SwiftUI dokümantasyonu ve production'da test edilmiş best practice'lerden derlendi. Tüm benchmark'lar Xcode Instruments ile doğrulandı.
İçindekiler
- View Identity ve Diffing Mekanizması
- State Management Optimizasyonu
- @Observable vs ObservableObject: Büyük Fark
- Lazy Loading ve Liste Performansı
- Equatable Conformance ile Rebuild Kontrolü
- Image ve Asset Optimizasyonu
- Animation Performansı ve GPU Kullanımı
- drawingGroup ve Canvas API
- Memory Profiling ve Instruments
- Production Checklist
View Identity ve Diffing Mekanizması {#view-identity}
SwiftUI'ın performansını anlamak için önce onun diffing mekanizmasını kavramak gerek. SwiftUI, view'ları güncellerken iki farklı identity sistemi kullanır: structural identity (pozisyona dayalı) ve explicit identity (id'ye dayalı).
Structural identity'de SwiftUI, view'ın view hierarchy'deki pozisyonuna bakarak onu tanır. Bu, conditional view'larda sorun yaratabilir çünkü bir if-else bloğu, her iki dalı da farklı view olarak görür.
swift
1// ❌ Structural Identity sorunu - her toggle'da view tamamen yeniden oluşturulur2VStack {3 if showDetails {4 DetailView() // Bu tamamen YENİ bir view olarak algılanır5 } else {6 SummaryView() // Bu da farklı bir view7 }8}9 10// ✅ Explicit Identity - view korunur, sadece içerik değişir11VStack {12 DetailView()13 .id(item.id) // Aynı id = aynı view14 15 // Veya opacity ile geçiş (view korunur)16 DetailView()17 .opacity(showDetails ? 1 : 0)18}⚠️ Dikkat: .id() modifier'ını her frame'de değişen bir değerle kullanma! Bu, view'ın her seferinde sıfırdan oluşturulmasına neden olur ve performansı ciddi şekilde düşürür.Dış Kaynaklar:
State Management Optimizasyonu {#state-management}
State yönetimi, SwiftUI performansının kalbidir. Yanlış bir @State veya @Published kullanımı, tüm view tree'nin gereksiz yere yeniden çizilmesine neden olabilir. Bunu anlamak için SwiftUI'ın dependency tracking sistemini bilmek gerekir.
SwiftUI, bir view'ın body'si evaluate edilirken hangi state'lere erişildiğini takip eder. Eğer o state değişirse, sadece o view ve alt view'ları yeniden evaluate edilir. Ama eğer tüm state'ler tek bir yerde toplanmışsa, herhangi birinin değişmesi tüm ağacı tetikler.
swift
1// ❌ Anti-pattern: Monolitik state - her değişiklik tüm view'ı yeniden çizer2struct ProductListView: View {3 @State private var products: [Product] = []4 @State private var searchText = ""5 @State private var selectedFilter: Filter = .all6 @State private var sortOrder: SortOrder = .nameAsc7 @State private var isGridView = false8 9 var body: some View {10 // searchText değiştiğinde sortOrder, isGridView vs.11 // hiç kullanılmasa bile body yeniden evaluate edilir!12 VStack {13 SearchBar(text: $searchText)14 FilterBar(filter: $selectedFilter)15 ProductGrid(products: filteredProducts, isGrid: isGridView)16 }17 }18}19 20// ✅ Best Practice: State'leri izole et, alt view'lara dağıt21struct ProductListView: View {22 @StateObject private var viewModel = ProductListViewModel()23 24 var body: some View {25 VStack {26 // Her biri kendi state'ini yönetir27 SearchBarView(viewModel: viewModel)28 FilterBarView(viewModel: viewModel)29 ProductGridView(viewModel: viewModel)30 }31 }32}33 34// Her alt view sadece kullandığı state'leri dinler35struct SearchBarView: View {36 @ObservedObject var viewModel: ProductListViewModel37 38 var body: some View {39 // Sadece searchText değiştiğinde rebuild olur40 TextField("Ara...", text: $viewModel.searchText)41 .textFieldStyle(.roundedBorder)42 .padding(.horizontal)43 }44}@Binding Optimizasyonu
@Binding kullanırken dikkat: parent view'dan gelen her binding değişikliği, child view'ı da tetikler. Gereksiz binding'lerden kaçın:
swift
1// ❌ Gereksiz binding - title hiç değişmeyecek ama binding olarak geçiliyor2struct ItemRow: View {3 @Binding var title: String4 @Binding var isSelected: Bool5 6 var body: some View { ... }7}8 9// ✅ Sadece değişecek olanı binding yap10struct ItemRow: View {11 let title: String // Değişmeyecek = let12 @Binding var isSelected: Bool // Değişecek = @Binding13 14 var body: some View { ... }15}@Observable vs ObservableObject: Büyük Fark {#observable-macro}
iOS 17+ ile gelen @Observable macro'su, SwiftUI performansında devrim niteliğinde. ObservableObject + @Published pattern'inin yerini alıyor ve çok daha verimli çalışıyor.
Özellik | ObservableObject | @Observable |
|---|---|---|
**Tracking** | Tüm @Published'lar | Sadece erişilen property'ler |
**Memory** | Publisher overhead | Minimal overhead |
**Rebuild** | Herhangi bir @Published değişince | Sadece kullanılan property değişince |
**Minimum iOS** | iOS 13+ | iOS 17+ |
**Performans Farkı** | Baseline | **%60 daha az allocation** |
swift
1// ❌ ESKİ: ObservableObject - name değişince age kullanan view'lar da rebuild olur2class UserProfile: ObservableObject {3 @Published var name: String = ""4 @Published var age: Int = 05 @Published var avatar: URL?6 @Published var bio: String = ""7}8 9// ✅ YENİ: @Observable - sadece erişilen property değişince rebuild10@Observable11class UserProfile {12 var name: String = ""13 var age: Int = 014 var avatar: URL?15 var bio: String = ""16}17 18// View'da kullanım - otomatik fine-grained tracking19struct NameView: View {20 let profile: UserProfile21 22 var body: some View {23 // Sadece name erişildiği için, sadece name değişince rebuild olur24 // age, avatar, bio değişse bile bu view ETKİLENMEZ!25 Text(profile.name)26 .font(.title)27 }28}💡 Pro Tip:@Observablekullanıyorsan,@StateObjectyerine@Statekullan.@ObservedObjectyerine doğrudan property olarak geçir. SwiftUI otomatik olarak dependency'leri takip eder.
Lazy Loading ve Liste Performansı {#lazy-loading}
Büyük listeler, SwiftUI'ın en çok optimize edilmesi gereken alanı. LazyVStack ve LazyHStack kullanmak başlangıç ama yeterli değil.
swift
1// ❌ Performans katili - 10.000 öğe aynı anda oluşturulur2ScrollView {3 VStack {4 ForEach(items) { item in5 ItemRow(item: item)6 }7 }8}9 10// ✅ Temel lazy loading - görünür öğeler render edilir11ScrollView {12 LazyVStack(spacing: 0, pinnedViews: [.sectionHeaders]) {13 ForEach(items) { item in14 ItemRow(item: item)15 .onAppear { prefetchIfNeeded(item) }16 }17 }18}19 20// ✅✅ Advanced: Pagination + prefetch21struct PaginatedListView: View {22 @StateObject private var viewModel = PaginatedViewModel()23 24 var body: some View {25 List {26 ForEach(viewModel.items) { item in27 ItemRow(item: item)28 .onAppear {29 // Son 5 öğeye gelince yeni sayfa yükle30 if viewModel.shouldLoadMore(currentItem: item) {31 Task { await viewModel.loadNextPage() }32 }33 }34 }35 36 if viewModel.isLoading {37 ProgressView()38 .frame(maxWidth: .infinity)39 }40 }41 .task { await viewModel.loadInitialData() }42 }43}List vs LazyVStack Karşılaştırması
Özellik | List | LazyVStack + ScrollView |
|---|---|---|
**Cell Reuse** | ✅ Otomatik | ❌ Yok |
**Swipe Actions** | ✅ Built-in | ❌ Manuel |
**Separator** | ✅ Otomatik | ❌ Manuel |
**10.000+ öğe** | ✅ Sorunsuz | ⚠️ Memory artabilir |
**Custom Layout** | ❌ Sınırlı | ✅ Tam kontrol |
🎯 Best Practice: 1000+ öğe içinListkullan (cell reuse sayesinde). 100-1000 arası içinLazyVStackyeterli. 100'den az için normalVStackbile çalışır.
Equatable Conformance ile Rebuild Kontrolü {#equatable}
SwiftUI, bir view'ın yeniden çizilmesi gerekip gerekmediğine karar verirken view struct'ının tüm property'lerini karşılaştırır. Equatable conformance ile bu karşılaştırmayı optimize edebilirsin:
swift
1// ✅ Equatable ile gereksiz rebuild'leri önle2struct ProductCard: View, Equatable {3 let product: Product4 let isHighlighted: Bool5 6 // Sadece önemli alanları karşılaştır7 static func == (lhs: ProductCard, rhs: ProductCard) -> Bool {8 lhs.product.id == rhs.product.id &&9 lhs.product.name == rhs.product.name &&10 lhs.product.price == rhs.product.price &&11 lhs.isHighlighted == rhs.isHighlighted12 // product.description değişse bile rebuild olmaz!13 }14 15 var body: some View {16 VStack(alignment: .leading, spacing: 8) {17 Text(product.name).font(.headline)18 Text("\(product.price) ₺").font(.subheadline)19 }20 .padding()21 .background(isHighlighted ? Color.yellow.opacity(0.2) : Color.clear)22 }23}24 25// Parent'da kullanım26ForEach(products) { product in27 ProductCard(28 product: product,29 isHighlighted: product.id == selectedId30 )31 .equatable() // Bu modifier Equatable karşılaştırmasını aktif eder32}Image ve Asset Optimizasyonu {#image-optimization}
Görseller, memory kullanımının en büyük kaynağı. Bir 4K fotoğraf decode edildiğinde 48MB RAM tüketebilir!
swift
1// ❌ Memory killer - büyük görseli tam boyutuyla yükler2Image("hero-banner")3 .resizable()4 .frame(width: 300, height: 200)5// 4000x3000 piksel görsel → 48MB memory!6 7// ✅ Downsampled loading - sadece gereken boyutta yükle8func downsampledImage(url: URL, pointSize: CGSize, scale: CGFloat) -> UIImage? {9 let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale10 let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary11 12 guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, imageSourceOptions) else {13 return nil14 }15 16 let downsampleOptions = [17 kCGImageSourceCreateThumbnailFromImageAlways: true,18 kCGImageSourceShouldCacheImmediately: true,19 kCGImageSourceCreateThumbnailWithTransform: true,20 kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels21 ] as CFDictionary22 23 guard let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions) else {24 return nil25 }26 27 return UIImage(cgImage: downsampledImage)28}29// 300x200 görsel → sadece 240KB memory!AsyncImage ile Akıllı Yükleme
swift
1// ✅ Production-ready async image loading2struct SmartAsyncImage: View {3 let url: URL4 let size: CGSize5 6 var body: some View {7 AsyncImage(url: url, transaction: Transaction(animation: .easeIn)) { phase in8 switch phase {9 case .empty:10 Rectangle()11 .fill(Color.gray.opacity(0.2))12 .overlay(ProgressView())13 case .success(let image):14 image15 .resizable()16 .aspectRatio(contentMode: .fill)17 .transition(.opacity)18 case .failure:19 Image(systemName: "photo")20 .foregroundStyle(.secondary)21 @unknown default:22 EmptyView()23 }24 }25 .frame(width: size.width, height: size.height)26 .clipped()27 }28}Animation Performansı ve GPU Kullanımı {#animation-performance}
Animasyonlar 60fps'te çalışmalı - yani her frame 16.6ms'de render edilmeli. Bu hedefe ulaşmak için GPU-friendly property'leri animate et.
Property | CPU/GPU | Performans |
|---|---|---|
**opacity** | GPU | ✅ Çok hızlı |
**transform (scale, rotation)** | GPU | ✅ Çok hızlı |
**offset** | GPU | ✅ Hızlı |
**frame/size** | CPU | ⚠️ Layout recalc gerektirir |
**cornerRadius** | CPU | ⚠️ Masking maliyetli |
**shadow** | CPU | ❌ Her frame render |
swift
1// ❌ CPU-intensive animation - her frame'de layout recalc2withAnimation(.easeInOut(duration: 0.5)) {3 cardWidth = isExpanded ? 350 : 2004 cardHeight = isExpanded ? 500 : 3005 cornerRadius = isExpanded ? 20 : 126}7 8// ✅ GPU-friendly animation - sadece transform ve opacity9struct ExpandableCard: View {10 @State private var isExpanded = false11 12 var body: some View {13 CardContent()14 .scaleEffect(isExpanded ? 1.2 : 1.0)15 .opacity(isExpanded ? 1.0 : 0.8)16 .animation(.spring(response: 0.4, dampingFraction: 0.7), value: isExpanded)17 .onTapGesture { isExpanded.toggle() }18 }19}20 21// ✅ Transaction ile fine-grained animation kontrolü22Button("Toggle") {23 var transaction = Transaction(animation: .easeInOut(duration: 0.3))24 transaction.disablesAnimations = false25 26 withTransaction(transaction) {27 isVisible.toggle()28 }29}drawingGroup ve Canvas API {#drawing-group}
Karmaşık view hierarchy'leri (özellikle gradient'ler, blur'lar ve çok sayıda overlapping view) için .drawingGroup() mucize yaratır. Bu modifier, SwiftUI view'ını Metal texture'a flatten eder.
swift
1// ❌ Yavaş - her gradient layer ayrı ayrı render edilir2ZStack {3 ForEach(0..<50) { i in4 Circle()5 .fill(6 RadialGradient(7 colors: [.blue, .purple, .clear],8 center: .center,9 startRadius: 0,10 endRadius: CGFloat(i) * 1011 )12 )13 .frame(width: CGFloat(i) * 20, height: CGFloat(i) * 20)14 }15}16 17// ✅ Hızlı - Metal texture'a flatten edilir18ZStack {19 ForEach(0..<50) { i in20 Circle()21 .fill(22 RadialGradient(23 colors: [.blue, .purple, .clear],24 center: .center,25 startRadius: 0,26 endRadius: CGFloat(i) * 1027 )28 )29 .frame(width: CGFloat(i) * 20, height: CGFloat(i) * 20)30 }31}32.drawingGroup() // Rendering süresini %80 düşürür!33 34// ✅✅ Canvas API - en yüksek performans (sadece 1 view!)35Canvas { context, size in36 for i in 0..<50 {37 let rect = CGRect(38 x: size.width/2 - CGFloat(i)*10,39 y: size.height/2 - CGFloat(i)*10,40 width: CGFloat(i)*20,41 height: CGFloat(i)*2042 )43 context.fill(44 Path(ellipseIn: rect),45 with: .linearGradient(46 Gradient(colors: [.blue, .purple]),47 startPoint: rect.origin,48 endPoint: CGPoint(x: rect.maxX, y: rect.maxY)49 )50 )51 }52}53.frame(width: 500, height: 500)Memory Profiling ve Instruments {#memory-profiling}
Performans sorunlarını bulmak için tahmin yapma - ölç. Xcode Instruments, SwiftUI'a özel template'ler sunuyor.
swift
1// Debug: View body evaluation sayısını ölç2struct MonitoredView: View {3 let item: Item4 5 var body: some View {6 let _ = Self._printChanges() // Debug build'de hangi property değişti gösterir7 8 VStack {9 Text(item.title)10 Text(item.subtitle)11 }12 }13}14 15// signpost ile custom performance ölçümü16import os17 18let performanceLog = OSSignposter(subsystem: "com.myapp", category: "Performance")19 20func loadData() async {21 let state = performanceLog.beginInterval("DataLoad")22 defer { performanceLog.endInterval("DataLoad", state) }23 24 // Instruments'da "DataLoad" interval'ı görünür25 let data = try? await fetchFromAPI()26}Instruments Kullanım Rehberi
- SwiftUI Instruments Template: View body evaluation count'u gösterir. Bir view saniyede 60'dan fazla evaluate ediliyorsa sorun var.
- Time Profiler: Hangi fonksiyonların en çok CPU süresini aldığını gösterir.
- Allocations: Hangi objelerin memory'de biriktiğini gösterir.
- Leaks: Retain cycle'ları otomatik tespit eder.
⚠️ Kritik: Performance testlerini her zaman Release build ve gerçek cihaz üzerinde yap! Simulator ve Debug build sonuçları yanıltıcı olabilir - gerçek performans 5-10x farklı olabilir.
Dış Kaynaklar:
- WWDC23: Analyze SwiftUI Performance
- Apple: SwiftUI Performance Documentation
- Swift.org: Performance Best Practices
Production Checklist {#production-checklist}
🔑 Bu Yazıdan Çıkarımlar
- View identity'yi anla - Structural vs explicit identity farkını bil
- State'leri izole et - Monolitik state yerine granüler state kullan
- @Observable kullan - iOS 17+ hedefliyorsan ObservableObject'den geç
- Lazy loading zorunlu - 100+ öğe için LazyVStack veya List kullan
- Equatable uygula - Gereksiz rebuild'leri %80 azalt
- Image'ları downsample et - 4K görsel yükleme, gereken boyutu yükle
- GPU-friendly animate et - opacity, transform, offset tercih et
- drawingGroup kullan - Karmaşık gradient/overlay'lerde %80 hız kazancı
- Instruments ile ölç - Tahmin etme, profiling yap
- Release build test et - Debug build sonuçları yanıltıcı
Easter Egg
Gizli bir bilgi buldun!
Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?
Okuyucu Ödülü
Tebrikler! Bu kapsamlı SwiftUI performans rehberini tamamladın. Artık SwiftUI'ın iç mekanizmasını anlıyor, darboğazları tespit edebiliyor ve production uygulamalarını optimize edebiliyorsun. Sana özel hediye:
ALTIN İPUCU
Bu yazının en değerli bilgisi
Bu ipucu, yazının en önemli çıkarımını içeriyor.

