SwiftUI ile Performans Optimizasyonu: Kapsamlı Rehber
SwiftUI ile Performans Optimizasyonu: Kapsamlı Rehber
SwiftUI; deklaratif yapısı, canlı önizleme ve çapraz platform desteği sayesinde yeni projeler için güçlü bir temel sunuyor. Ancak uygulama karmaşıklığı arttıkça performans sorunları kendini göstermeye başlıyor. Bu kapsamlı rehberde günlük projelerimde kullandığım detaylı optimizasyon tekniklerini, gerçek kod örnekleriyle birlikte paylaşmak istiyorum.
İçindekiler
1. View Yapısını Optimize Edin 2. State Management Performansı 3. Ağ İşlemlerini Verimli Yönetin 4. Animasyon Optimizasyonu 5. Özel Çizim ve Canvas 6. Liste ve Koleksiyon Performansı 7. Profiling ve Ölçümleme1. View Yapısını Optimize Edin
Problem
BirView dosyasındaki "body" bloğu büyümeye başladığında SwiftUI'nın diffing algoritması tüm view hiyerarşisini yeniden değerlendirmek zorunda kalır. Bu durum gereksiz yeniden çizimler ve performans sorunlarına yol açar.Çözüm: View Ayrıştırma (View Extraction)
swift
// ❌ KÖTÜ: Tek bir büyük view
struct DashboardView: View {
@State private var selectedTab = 0
@State private var userStats: [Stat] = [] var body: some View {
VStack(spacing: 20) {
// 100+ satır header kodu
HStack {
Image(systemName: "person.circle")
Text("Hoş geldin, Ali")
Spacer()
Button("Ayarlar") { }
}
.padding()
// 150+ satır metrics kodu
LazyVGrid(columns: [GridItem(.adaptive(minimum: 150))]) {
ForEach(userStats) { stat in
VStack {
Text(stat.value)
.font(.title)
Text(stat.label)
.font(.caption)
}
.padding()
.background(Color.blue.opacity(0.1))
.cornerRadius(12)
}
}
// 200+ satır activities kodu
ScrollView {
ForEach(recentActivities) { activity in
HStack {
Image(systemName: activity.icon)
VStack(alignment: .leading) {
Text(activity.title)
Text(activity.date)
.font(.caption)
}
}
}
}
}
}
}
// ✅ İYİ: Ayrıştırılmış view'lar
struct DashboardView: View {
var body: some View {
ScrollView {
VStack(spacing: 20) {
DashboardHeader()
MetricsGrid()
RecentActivitiesSection()
}
}
}
}
// Ayrı view'lar sadece kendi state'leri değiştiğinde yeniden çizilir
struct DashboardHeader: View {
@EnvironmentObject var userManager: UserManager
var body: some View {
HStack {
AsyncImage(url: userManager.user.avatarURL) { image in
image.resizable()
.aspectRatio(contentMode: .fill)
} placeholder: {
Image(systemName: "person.circle.fill")
}
.frame(width: 50, height: 50)
.clipShape(Circle())
VStack(alignment: .leading) {
Text("Hoş geldin,")
.font(.caption)
.foregroundColor(.secondary)
Text(userManager.user.name)
.font(.headline)
}
Spacer()
NavigationLink("Ayarlar") {
SettingsView()
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(16)
.shadow(radius: 2)
}
}
struct MetricsGrid: View {
@StateObject private var viewModel = MetricsViewModel()
var body: some View {
LazyVGrid(
columns: [
GridItem(.adaptive(minimum: 150), spacing: 16)
],
spacing: 16
) {
ForEach(viewModel.metrics) { metric in
MetricCard(metric: metric)
}
}
.padding(.horizontal)
.task {
await viewModel.loadMetrics()
}
}
}
struct MetricCard: View {
let metric: Metric
var body: some View {
VStack(spacing: 8) {
Image(systemName: metric.icon)
.font(.system(size: 30))
.foregroundColor(.blue)
Text(metric.value)
.font(.title2)
.fontWeight(.bold)
Text(metric.label)
.font(.caption)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity)
.padding()
.background(
RoundedRectangle(cornerRadius: 12)
.fill(Color.blue.opacity(0.1))
)
}
}
İleri Seviye: EquatableView Kullanımı
swift
// Pahalı hesaplamalar içeren view'lar için Equatable protokolü
struct ExpensiveChartView: View, Equatable {
let dataPoints: [DataPoint]
let style: ChartStyle static func == (lhs: ExpensiveChartView, rhs: ExpensiveChartView) -> Bool {
// Sadece gerçekten değişen özellikleri karşılaştır
return lhs.dataPoints.count == rhs.dataPoints.count &&
lhs.style == rhs.style
}
var body: some View {
Canvas { context, size in
// Pahalı çizim işlemleri
drawComplexChart(context, size, dataPoints)
}
}
}
struct ParentView: View {
@State private var dataPoints: [DataPoint] = []
@State private var unrelatedCounter = 0
var body: some View {
VStack {
// equatable() modifier sayesinde dataPoints değişmediğinde
// ExpensiveChartView yeniden çizilmez
ExpensiveChartView(dataPoints: dataPoints, style: .line)
.equatable()
Button("Sayaç: \(unrelatedCounter)") {
unrelatedCounter += 1
}
}
}
}
2. State Management Performansı
@StateObject vs @ObservedObject Farkı
swift
// ❌ KÖTÜ: Parent view her çizildiğinde ViewModel yeniden oluşturulur
struct ProductListView: View {
@ObservedObject var viewModel = ProductViewModel() // Yanlış! var body: some View {
List(viewModel.products) { product in
ProductRow(product: product)
}
}
}
// ✅ İYİ: ViewModel yalnızca bir kez oluşturulur
struct ProductListView: View {
@StateObject private var viewModel = ProductViewModel()
var body: some View {
List(viewModel.products) { product in
ProductRow(product: product)
}
}
}
// Parent'tan gelen ViewModel için @ObservedObject kullan
struct ProductDetailView: View {
@ObservedObject var viewModel: ProductViewModel // Doğru kullanım
var body: some View {
ScrollView {
VStack {
Text(viewModel.product.name)
Text(viewModel.product.description)
}
}
}
}
@Published Optimizasyonu
swift
class UserViewModel: ObservableObject {
// ❌ KÖTÜ: Her küçük değişiklik tüm view'ı yeniden çizer
@Published var user: User // ✅ İYİ: Sadece ilgili özellikler değiştiğinde bildirim gönder
@Published var userName: String
@Published var userEmail: String
@Published var userAvatar: URL?
// Pahalı hesaplamalar için Combine kullan
@Published var searchText = ""
@Published private(set) var filteredUsers: [User] = []
private var cancellables = Set()
init() {
// Debounce ile gereksiz aramalardan kaçın
$searchText
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
.removeDuplicates()
.sink { [weak self] text in
self?.performSearch(text)
}
.store(in: &cancellables)
}
private func performSearch(_ text: String) {
// Arama işlemi
filteredUsers = allUsers.filter { user in
user.name.localizedCaseInsensitiveContains(text)
}
}
}
Environment Objects Optimizasyonu
swift
// ❌ KÖTÜ: Tüm app state tek bir büyük object'te
class AppState: ObservableObject {
@Published var user: User?
@Published var cart: Cart
@Published var products: [Product]
@Published var settings: Settings
@Published var notifications: [Notification]
// 20+ başka property...
}// ✅ İYİ: Sorumluluklara göre ayrılmış state'ler
class AuthenticationState: ObservableObject {
@Published var user: User?
@Published var isAuthenticated: Bool = false
}
class ShoppingCartState: ObservableObject {
@Published var items: [CartItem] = []
@Published var totalPrice: Decimal = 0
}
class ProductCatalogState: ObservableObject {
@Published var products: [Product] = []
@Published var categories: [Category] = []
}
// Her view sadece ihtiyacı olan state'i kullanır
struct CartView: View {
@EnvironmentObject var cartState: ShoppingCartState
// AuthenticationState veya ProductCatalogState değişse bile
// bu view yeniden çizilmez
var body: some View {
List(cartState.items) { item in
CartItemRow(item: item)
}
}
}
3. Ağ İşlemlerini Verimli Yönetin
Async/Await ile Background Threading
swift
@MainActor
class NewsViewModel: ObservableObject {
@Published var articles: [Article] = []
@Published var isLoading = false
@Published var error: Error? // ✅ Paralel yükleme ile performans artışı
func loadAllData() async {
isLoading = true
defer { isLoading = false }
do {
// Birden fazla API çağrısını paralel olarak yap
async let topNews = NewsAPI.fetchTopNews()
async let trendingNews = NewsAPI.fetchTrending()
async let localNews = NewsAPI.fetchLocalNews()
// Hepsi bittiğinde sonuçları birleştir
let (top, trending, local) = try await (topNews, trendingNews, localNews)
articles = top + trending + local
} catch {
self.error = error
}
}
// ✅ Task Group ile dinamik paralel işlemler
func loadArticlesByCategory(categories: [String]) async {
isLoading = true
defer { isLoading = false }
do {
articles = try await withThrowingTaskGroup(
of: [Article].self
) { group in
// Her kategori için paralel task oluştur
for category in categories {
group.addTask {
try await NewsAPI.fetchArticles(category: category)
}
}
// Tüm sonuçları topla
var allArticles: [Article] = []
for try await categoryArticles in group {
allArticles.append(contentsOf: categoryArticles)
}
return allArticles
}
} catch {
self.error = error
}
}
// ✅ İptal edilebilir task'lar
private var loadTask: Task?
func searchArticles(query: String) {
// Önceki aramayı iptal et
loadTask?.cancel()
loadTask = Task { @MainActor in
do {
// Debounce için kısa bir bekleme
try await Task.sleep(nanoseconds: 300_000_000)
// İptal kontrolü
guard !Task.isCancelled else { return }
articles = try await NewsAPI.search(query: query)
} catch {
if !Task.isCancelled {
self.error = error
}
}
}
}
}
Image Caching Sistemi
swift
// Görsel yüklemesi için cache sistemi
actor ImageCache {
private var cache: [URL: UIImage] = [:]
private let maxCacheSize = 100 func image(for url: URL) -> UIImage? {
return cache[url]
}
func setImage(_ image: UIImage, for url: URL) {
// Cache boyut kontrolü
if cache.count >= maxCacheSize {
// En eski girişi sil (basit FIFO)
if let firstKey = cache.keys.first {
cache.removeValue(forKey: firstKey)
}
}
cache[url] = image
}
func clear() {
cache.removeAll()
}
}
struct CachedAsyncImage: View {
let url: URL
@State private var image: UIImage?
@State private var isLoading = false
static let cache = ImageCache()
var body: some View {
Group {
if let image = image {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fill)
} else if isLoading {
ProgressView()
} else {
Color.gray.opacity(0.2)
}
}
.task {
await loadImage()
}
}
private func loadImage() async {
isLoading = true
defer { isLoading = false }
// Önce cache'e bak
if let cachedImage = await Self.cache.image(for: url) {
image = cachedImage
return
}
// Cache'te yoksa indir
do {
let (data, _) = try await URLSession.shared.data(from: url)
// Background thread'de decode et
let decodedImage = await Task.detached(priority: .userInitiated) {
UIImage(data: data)
}.value
if let decodedImage = decodedImage {
await Self.cache.setImage(decodedImage, for: url)
image = decodedImage
}
} catch {
print("Görsel yüklenemedi: \(error)")
}
}
}
4. Animasyon Optimizasyonu
Implicit vs Explicit Animations
swift
struct AnimationOptimizationView: View {
@State private var isExpanded = false
@State private var scale: CGFloat = 1.0 var body: some View {
VStack {
// ❌ KÖTÜ: Her state değişikliğinde tüm view animasyonlu
Text("Başlık")
.scaleEffect(scale)
.animation(.spring(), value: scale) // Dikkat!
// ✅ İYİ: Sadece ilgili değişiklik animasyonlu
Text("Başlık")
.scaleEffect(scale)
Button("Büyüt") {
withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
scale = scale == 1.0 ? 1.5 : 1.0
}
}
}
}
}
// Performanslı hero animasyon
struct HeroAnimationView: View {
@Namespace private var animation
@State private var showDetail = false
var body: some View {
ZStack {
if !showDetail {
VStack {
ForEach(items) { item in
ItemThumbnail(item: item)
.matchedGeometryEffect(
id: item.id,
in: animation
)
.onTapGesture {
withAnimation(.spring(response: 0.4)) {
showDetail = true
}
}
}
}
} else {
ItemDetailView(item: selectedItem)
.matchedGeometryEffect(
id: selectedItem.id,
in: animation
)
}
}
}
}
Gesture Optimizasyonu
swift
struct SwipeableCard: View {
@State private var offset = CGSize.zero
@State private var isDragging = false var body: some View {
CardContent()
.offset(offset)
.rotationEffect(.degrees(Double(offset.width / 20)))
.gesture(
DragGesture()
.onChanged { gesture in
isDragging = true
// Animasyon olmadan hemen güncelle (smooth gesture)
offset = gesture.translation
}
.onEnded { gesture in
isDragging = false
// Gesture bittiğinde animasyonlu geçiş
withAnimation(.spring()) {
if abs(gesture.translation.width) > 100 {
// Kaydırma yeterince büyük, kartı at
offset = CGSize(
width: gesture.translation.width > 0 ? 500 : -500,
height: 0
)
} else {
// Geri getir
offset = .zero
}
}
}
)
}
}
5. Özel Çizim ve Canvas
swift
struct PerformantChartView: View {
let dataPoints: [Double] var body: some View {
Canvas { context, size in
// drawingGroup() kullanmadan önce profil yap!
let path = createChartPath(dataPoints: dataPoints, size: size)
context.stroke(
path,
with: .color(.blue),
lineWidth: 2
)
// Gradient fill
context.fill(
path,
with: .linearGradient(
Gradient(colors: [.blue.opacity(0.3), .clear]),
startPoint: CGPoint(x: 0, y: 0),
endPoint: CGPoint(x: 0, y: size.height)
)
)
}
.frame(height: 200)
}
private func createChartPath(dataPoints: [Double], size: CGSize) -> Path {
var path = Path()
guard !dataPoints.isEmpty else { return path }
let maxValue = dataPoints.max() ?? 1
let stepX = size.width / CGFloat(dataPoints.count
- 1)
// İlk nokta
let firstY = size.height - (CGFloat(dataPoints[0]) / CGFloat(maxValue) * size.height)
path.move(to: CGPoint(x: 0, y: firstY)) // Diğer noktalar
for (index, value) in dataPoints.enumerated() {
let x = CGFloat(index) * stepX
let y = size.height
- (CGFloat(value) / CGFloat(maxValue) * size.height)
path.addLine(to: CGPoint(x: x, y: y))
} return path
}
}
// drawingGroup kullanımı
- dikkatli kullan!
struct ComplexDrawingView: View {
var body: some View {
Canvas { context, size in
// Çok sayıda shape çizimi
for i in 0..<1000 {
let rect = CGRect(
x: CGFloat.random(in: 0...size.width),
y: CGFloat.random(in: 0...size.height),
width: 10,
height: 10
)
context.fill(Path(rect), with: .color(.blue))
}
}
.drawingGroup() // Metal rendering pipeline kullan
// ⚠️ Uyarı: Bu her zaman daha hızlı olmayabilir, profil yapın!
}
}
6. Liste ve Koleksiyon Performansı
swift
// LazyVStack vs VStack performans karşılaştırması
struct ContactsListView: View {
let contacts: [Contact] var body: some View {
ScrollView {
// ✅ LazyVStack - Sadece görünen öğeler render edilir
LazyVStack(spacing: 12) {
ForEach(contacts) { contact in
ContactRow(contact: contact)
}
}
.padding()
}
}
}
// Gelişmiş LazyVGrid kullanımı
struct PhotoGridView: View {
let photos: [Photo]
@State private var selectedPhoto: Photo?
// Adaptive columns ile responsive grid
private let columns = [
GridItem(.adaptive(minimum: 100, maximum: 200), spacing: 8)
]
var body: some View {
ScrollView {
LazyVGrid(columns: columns, spacing: 8) {
ForEach(photos) { photo in
PhotoThumbnail(photo: photo)
.aspectRatio(1, contentMode: .fill)
.clipped()
.cornerRadius(8)
.onTapGesture {
selectedPhoto = photo
}
}
}
.padding()
}
}
}
// List optimizasyonu ile SwipeActions
struct OptimizedListView: View {
@StateObject private var viewModel = ItemsViewModel()
var body: some View {
List {
ForEach(viewModel.items) { item in
ItemRow(item: item)
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button(role: .destructive) {
viewModel.deleteItem(item)
} label: {
Label("Sil", systemImage: "trash")
}
}
.swipeActions(edge: .leading) {
Button {
viewModel.favoriteItem(item)
} label: {
Label("Favorile", systemImage: "star")
}
.tint(.yellow)
}
}
// Sonsuz scroll için
if viewModel.hasMore {
ProgressView()
.onAppear {
Task {
await viewModel.loadMore()
}
}
}
}
.listStyle(.insetGrouped)
.refreshable {
await viewModel.refresh()
}
}
}
7. Instruments ile Profiling
Time Profiler Kullanımı
1. Xcode menüsünden: Product > Profile (⌘ + I) 2. Time Profiler template'ini seçin 3. Record butonuna basıp uygulamanızı kullanın 4. Call Tree ayarlarını yapılandırın:
- ✅ Hide System Libraries
- ✅ Flatten Recursion
Allocations ile Memory Profiling
swift
func setupObserver() { // ❌ KÖTÜ: Retain cycle onUpdate = { self.updateUI() // Strong reference! } } }
class ViewModelFixed { var onUpdate: (() -> Void)?
func setupObserver() { // ✅ İYİ: Weak reference onUpdate = { [weak self] in self?.updateUI() } } }
SwiftUI View Inspector
swift
// Debug modifiers
struct DebugView: View {
var body: some View {
ContentView()
.onAppear {
// View lifecycle tracking
print("View appeared: \(Self.self)")
}
.onDisappear {
print("View disappeared: \(Self.self)")
}
.onChange(of: someState) { oldValue, newValue in
print("State changed: \(oldValue) -> \(newValue)")
}
}
}// Custom debug modifier
extension View {
func debugPrint(_ value: Any) -> some View {
print("🔍 Debug:", value)
return self
}
func measureRenderTime() -> some View {
self.background(
GeometryReader { _ in
Color.clear.onAppear {
print("⏱️ Render time: \(Date())")
}
}
)
}
}
Sonuç ve Best Practices Özeti
✅ Yapılması Gerekenler
1. View'ları küçük parçalara bölün- Her view tek bir sorumluluğa sahip olmalı 2. Doğru state management kullanın
- @StateObject vs @ObservedObject farkını bilin
- Ağır işlemleri main thread'den ayırın 4. Cache kullanın
- Görseller ve API yanıtları için cache mekanizması
- Büyük listeler için LazyVStack/LazyVGrid kullanın 6. Profiling yapın
- Varsayımlarla değil, ölçümlerle optimize edin
❌ Yapılmaması Gerekenler
.animation() modifier'ı kullanmak
2. Tek bir büyük ObservableObject ile tüm state'i yönetmek
3. Main thread'de ağır hesaplamalar yapmak
4. View body'sinde karmaşık hesaplamalar
5. Gereksiz yeniden çizimler (unnecessary re-renders)
6. Memory leak'lere dikkat etmemekPerformans Kontrol Listesi
- [ ] View hiyerarşisi optimize edilmiş mi?
- [ ] State management doğru kullanılıyor mu?
- [ ] Görsel cache sistemi var mı?
- [ ] Ağ çağrıları paralel mi?
- [ ] Listeler lazy loading kullanıyor mu?
- [ ] Animasyonlar gerektiğinde mi uygulanıyor?
- [ ] Memory leak kontrolü yapıldı mı?
- [ ] Instruments ile profiling yapıldı mı?
---
Bu teknikleri düzenli olarak uyguladığınızda SwiftUI projelerinizde kare hızının yükseldiğini, veri akışlarının daha akıcı hâle geldiğini ve kullanıcı deneyiminin belirgin şekilde iyileştiğini göreceksiniz.
Unutmayın: Erken optimizasyon kötülüğün köküdür, önce çalışan kod yazın, sonra profiling ile darboğazları tespit edip optimize edin.
Siz hangi optimizasyon tekniklerini kullanıyorsunuz? Başka sorularınız varsa bana yazın, birlikte tartışalım! 🚀
Kaynaklar ve İleri Okuma
- [Apple - Improving Performance](https://developer.apple.com/documentation/swiftui/improving-performance)
- [WWDC - SwiftUI Performance](https://developer.apple.com/videos/play/wwdc2021/10022/)