App Store Connect’e her sabah bakarım. Portföyümdeki 12 uygulamanın crash-free session oranı birkaç yıldır %100 olarak kalıyor. Bu şans değil, kasıtlı bir disiplin. Bu yazıda kullandığım teknikleri paylaşacağım.
Nil’i koru, throw’u tercih et
Swift’te en yaygın crash sebebi hala nil unwrapping. Force-unwrap (!) crash’e direkt davetiye. Kod tabanında ! kullanımını neredeyse sıfıra indiriyorum.
Sadece IBOutlet’lerde @IBOutlet weak var x: UILabel! kullanıyorum (Apple’ın resmi yaklaşımı). Geri kalan her yerde optional’ı açıkça handle ediyorum, guard let, if let, nil coalescing (??), veya default bir fallback ile.
Network response’u her zaman şüpheli kabul et
Backend ne kadar sağlam olsa da, bir gün yanlış bir response dönecek. “Her zaman bu field gelir” varsayımı yapmayın.
Required field’ları Codable ile güvenceye al, optional field’ları optional yap. Backend yanlış bir şey gönderirse DecodingError al, crash olma. Ardından logging/analytics ile takip et.
Array’e index ile erişme
Swift’te array[5] eğer array 5 eleman altındaysa crash eder. Safe subscript extension’ı yazıp her projede kullanıyorum:
subscript(safe index: Int) -> Element?, index valid değilse nil dönüyor, crash etmiyor.
Bu extension’ı her projemde ilk günden ekliyorum. Kod biraz daha uzun yazılıyor ama bir daha “index out of range” crash’i görmezsin.
Background task’larda throw’u yutma
Task { } içindeki hataları genelde kullanıcı görmez. Bu yüzden geliştiriciler bunları yutmaya meyilli. Ama yutulan bir hata bir süre sonra state inconsistency’e döner ve asıl o state inconsistency crash’e yol açar.
Her catch bloğu ya recovery yapmalı ya log’lamalı. Boş catch { } kod tabanında kabul edilmez. Minimum Crashlytics veya Sentry’ye recordError çağrısı olmalı.
Main thread violation’ları erkeninde yakala
UIKit ve SwiftUI view operasyonları main thread’de olmalı. Başka bir thread’de view update ederseniz genelde anlık crash değil, rastgele davranış görürsünüz, bu production’da çok daha kötü.
Swift 6’da concurrency checking bu hataları compile-time’da yakalıyor. Eski projelerde assert(Thread.isMainThread) pattern’ini kullanıyorum. Debug build’de assert crash eder, development sırasında bulursunuz. Production’da assert hiçbir şey yapmaz, kullanıcıda crash olmaz.
Memory management’a asla güvenme
Swift ARC tarafında iyi, ama closure retain cycle’ları hala kolayca oluşuyor. Özellikle view controller’lar içindeki async closure’larda [weak self] kullanmak standart:
ViewModel callback’leri, network completion handler’ları, timer’lar, observation closure’ları, hepsinde weak self. Tek istisna: closure’ın view controller’ın ömründen daha kısa sürede çalışacağı garanti ise.
Instruments’ın Leaks ve Allocations araçlarını yeni feature’ın her release’inden önce çalıştırıyorum. 5 dakikalık bir check, ama memory leak’i production’a vermek çok daha pahalıya mal oluyor.
Test etmek için TestFlight’ı gerçekten kullan
TestFlight’a gönderdiğim her build en az 1 hafta testerlarda kalıyor. Kendi cihazımda çalışıyor olması yetmiyor, 3 farklı cihaz tipi (iPhone SE, iPhone 15, iPad), 2 farklı iOS versiyonu (current, current-1), farklı network koşulları.
Releası tester’lara dağıttıktan sonra App Store Connect’teki “Crashes” section’ı takip ederim. TestFlight’ta bir crash bile varsa release’i durduruyorum.
Crash reporting aracını doğru konfigüre et
Firebase Crashlytics veya Sentry, her ikisi de iyi. Önemli olan ikisini de doğru konfigüre etmek:
- User ID’yi (anonymized) her crash’e ekle
- Breadcrumb’ları kullan: son 5 UI aksiyonu crash report’una eklensin
- Custom key’ler ekle: hangi feature’dayken crash olduğu
- Sampling’i açma, her crash’i al
Bu context olmadan bir crash report’u “nerede oldu” söylüyor ama “niye oldu” söylemiyor. Bir crash report’u context olmadan ham veri.
Sonuç
%0 crash rate bir metrik değil, bir alışkanlık bütünü. Her PR review’da “bu crash’e yol açar mı?” sorusu. Her deploy öncesi Instruments check. Her release sonrası crash dashboard monitoring.
Pahalı bir şey değil, sadece disiplin.