Back to Articles
iOS DevelopmentSwiftUIPerformansiOS

SwiftUI ile Performans Optimizasyonu: Kapsamlı Rehber

Updated: Feb 10, 202525 dk okuma
SwiftUI Performance Optimization Tips

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çümleme

1. View Yapısını Optimize Edin

Problem

Bir View 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:

  • ✅ Separate by Thread
    • ✅ Hide System Libraries
    • ✅ Flatten Recursion

      Allocations ile Memory Profiling

      swift
    // Memory leak tespiti için class ViewModelWithLeak { var onUpdate: (() -> Void)?

    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
    3. Background threading
    • Ağır işlemleri main thread'den ayırın
    • 4. Cache kullanın
    • Görseller ve API yanıtları için cache mekanizması
    5. Lazy loading
    • 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

    1. Her yerde .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 etmemek

    Performans 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/)
  • [Instruments User Guide](https://help.apple.com/instruments/mac/)