Tüm Yazılar
KategoriiOS
Okuma Süresi
9 dk
Yayın Tarihi
2026-10-15
Kelime Sayısı
1.452kelime

Kahveni hazırla - bu içerikli bir makale!

NotificationKit 2.0: iOS'un 64 bildirim sınırının ötesinde planlama

Özet

NotificationKit 2.0 neden 64'ün ötesini kuyrukta tutuyor, sessiz saati tetiklenme anına uyguluyor ve UserNotifications'ın çöktüğü yerde hata fırlatıyor?

  • UserNotifications'ın bazı davranışları size haber vermeden başarısız olur. iOS bir uygulamanın yalnız en yakın zamanda tetiklenecek 64 bekleyen yerel bildirimini tutar, gerisini hata vermeden atar.
  • NotificationCenterClient, paketin sistemden istediklerini listeler: izin, ayarlar, istek ekleme ve silme, kategoriler ve badge.
  • README'deki hızlı başlangıç, yeni bir iOS uygulaması için eksiksiz bir ContentView.swift.
  • README'deki yol haritasında Swift Package Index kaydı, Live Activity'ler için broadcast push kanalları ve NotificationKitAPNs'in swift-crypto ile Linux derlemesi var.
NotificationKit 2.0: iOS'un 64 bildirim sınırının ötesinde planlama

İçindekiler


Problem

UserNotifications'ın bazı davranışları size haber vermeden başarısız olur. iOS bir uygulamanın yalnız en yakın zamanda tetiklenecek 64 bekleyen yerel bildirimini tutar, gerisini hata vermeden atar. Önümüzdeki üç ayın her günü için tek seferlik hatırlatıcı planlayan bir uygulama sisteme 64'ten fazlasını verir ve sonrakileri sessizce kaybeder. Bazı geçersiz değerler de hata fırlatmaz: 60 saniyeden kısa tekrarlayan bir UNTimeIntervalNotificationTrigger ya da sıfır, negatif veya NaN bir interval Objective-C exception'ı fırlatır; bu da bir catch'e ulaşmak yerine süreci sonlandırır. UNUserNotificationCenter delegate'i ise Swift 6 concurrency'ye ve unit test'lere uymayan bir callback kümesidir, çünkü gerçek center bir host uygulama ister.

iOS-Notification-Framework reposundaki Swift paketi NotificationKit, UserNotifications'ın üzerinde bu dört noktayı ele alan bir katman. 2.0.0 sürümü 15 Ekim 2026'da yayınlandı. İki library'si var: uygulamalar için NotificationKit (iOS 17, macOS 14, watchOS 10, visionOS 1) ve push gönderen sunucular ile komut satırı araçları için NotificationKitAPNs. İkisinin de Apple SDK'sı dışında bağımlılığı yok.

2.0'ın arkasında bir geçmiş var. Repo Ağustos 2025'te bir prototip olarak açıldı, 1.0.0 Şubat 2026'da yayınlandı, Haziran 2026'da küçük tipler eklendi. Yeniden yazımı başlatan commit 7aa405b 6.173 satırı siliyor ve nedenini yazıyor: her tip doğrudan UNUserNotificationCenter.current(), UserDefaults.standard ve Date()'e uzanıyordu, bu yüzden hiçbir şey unit test'te çalıştırılamıyordu; testler de yalnız builder'ların set ettiği özellikleri geri okuyordu. Bu yazının geri kalanı onun yerine gelen tasarımı anlatıyor.

Tasarım kararları ve alternatifler

Sisteme yalnız bir protocol üzerinden ulaşmak

NotificationCenterClient, paketin sistemden istediklerini listeler: izin, ayarlar, istek ekleme ve silme, kategoriler ve badge. UserNotificationCenterClient bunu UNUserNotificationCenter'a eşler; InMemoryNotificationCenter testlerde ve SwiftUI önizlemelerinde onun yerine geçer. In-memory center, planlamanın bağlı olduğu yerlerde sistemi taklit eder: yalnız en yakın 64 bekleyen bildirimi tutar, düşürdüklerini kaydeder, anlık bildirimleri hemen teslim eder ve aynı geçersiz değerleri reddeder.

Alternatif, Apple'ın bunun için tasarlamadığı UNUserNotificationCenter'ı doğrudan mock'lamak ya da planlamayı test etmeden bırakmaktı. Test double'ının bedeli, gerçek center'dan ayrışabilmesi. Repo bu açığı iki yoldan daraltıyor: SystemMappingTests değerlerin UNNotificationRequest'e nasıl dönüştüğünü kontrol ediyor, NotificationLab örnek uygulamasındaki üç UI testi de simülatörde gerçek center'a karşı çalışıyor.

64 slot'un önünde bir kuyruk

NotificationScheduler, planladığı her bildirimi kaydedilen bir durumda tutan bir actor. Diğer kodun eklediği bildirimlerin yanına sığan en yakın olanları sisteme verir, gerisini kuyruğa alır. refresh() teslim edilenleri unutur, kuyruktakileri boşalan slot'lara taşır ve daha erken bir bildirim slot'a ihtiyaç duyduğunda daha geç olanı kuyruğa geri alır.

Kararın kendisi iki saf tipte. SlotPlanner sistemin hangi bildirimleri tutacağına, FireDateResolver sessiz saatlere ve sıklık sınırına karar verir. İkisi de cihaz olmadan test ediliyor. Plan uygulanırken silmeler eklemelerden önce yapılır; böylece sistem hiçbir anda 64'ten fazlasını tutmaz ve kendi başına bir bildirim atmaz.

Durum bir NotificationStore'a yazılır. FileNotificationStore JSON'u Application Support'a atomik olarak yazar ve 8 MB'tan büyük dosyaları reddeder; InMemoryNotificationStore testler içindir.

Çağrılar tek bir actor'da değil, sırayla çalışır

Burada yalnız actor yetmiyor. Actor'lar her await'te reentrant'tır ve planlama center'ı birkaç kez bekler; iki eşzamanlı schedule çağrısı birbirinin arasına girip aynı slot'u iki kez verebilir. Private bir serialized yardımcısı her işlemi kendinden önce başlayanın arkasına zincirler. Bir test 100 bildirimi eşzamanlı planlayıp sonucu kontrol ediyor. Bedeli: yavaş bir çağrı bir sonrakini bekletir.

Politika tetiklenme tarihine uygulanır

Yerel bildirim tetiklendiğinde uygulamanın kodu çalışmaz; yani sessiz saat kararı teslim anında verilemez. FireDateResolver kararı planlama anında verir, ama şu anki saate değil bildirimin tetikleneceği tarihe bakar. 22:00–07:30 penceresinde, 21:00'de 23:00 için kurulan tek seferlik bir hatırlatıcı 07:30'a kayar. Tekrarlayan bildirimler kullanıcının seçtiği saati korur; time-sensitive ve critical bildirimler varsayılan olarak sessiz saatten geçer.

FrequencyCap, belirli uzunluktaki herhangi bir pencerede en fazla N bildirime izin verir ve hem öncekileri hem sonrakileri sayar. Cevap yalnız istenen tarih ya da mevcut bir bildirimin bir interval sonrası olabileceği için arama yalnız bu adaylara ikili arama ile bakar ve her zaman biter. Parametreli bir test sonucu 40 rastgele planda brute-force aramayla karşılaştırıyor.

Tipli hata ile doğrulama

LocalNotification.validate() ve NotificationTrigger.validate(), sistemin çöktüğü ya da yok saydığı değerler için NotificationValidationError fırlatır: kısa ya da sonlu olmayan interval'ler, aralık dışı tarih bileşenleri, geçmişteki tek seferlik tarihler, 0...1 dışındaki relevance score ve ses düzeyleri, geçersiz bölgeler. Özel veri [AnyHashable: Any] yerine bir JSONValue; böylece Sendable ve Codable oluyor ve kuyrukla birlikte kaydedilebiliyor. Dönüştürme 32 iç içe seviyede ve 10.000 değerde durur. İki center da aynı validate()'i çağırır; test double'ı sistemin reddedeceğini reddeder.

Uygulama ve sunucu için ayrı library'ler

NotificationKitAPNs APNs client'ını taşır: URLSession üzerinden HTTP/2, CryptoKit ile imzalanıp 50 dakika yeniden kullanılan ES256 provider token'ları, ExpiredProviderToken'da bir kez yeniden deneme. Ayrı bir library olması imzalama kodunu uygulamaların dışında tutuyor; PushPayload ve DeviceToken ise ortak, yani sunucu uygulamanın okuduğu tipin aynısını encode ediyor. Yalnız Apple framework'lerini kullanmak bir bağımlılık ağacından kaçınıyor; bedeli Linux desteği, README bunu yol haritasında listeliyor.

CI'ın bulduğu bir hata

Yeniden yazım PR #31 olarak açıldı ve ilk koşusu kırmızıydı. Hatalardan biri toolchain'ler hakkında iyi bir ders.

Notification Service Extension için temel sınıf olan MediaAttachmentService, medya indirmesini capture list'inde özgün bildirim içeriğini, downloader'ı ve teslim nesnesini tutan bir Task'ta başlatıyordu. Yerelde Swift 6.4 ile derleniyordu. CI runner'ında Swift 6.3 (Xcode 26.6) "pattern that the region-based isolation checker does not understand" hatasıyla durdu.

93efabc düzeltmesi girdileri private bir MediaJob değerine topluyor; task artık tek bir Sendable değer yakalıyor. Uygunluk @unchecked ve yorum bunun neden geçerli olduğunu yazıyor: UNNotificationContent yalnız okunuyor ve mutableCopy ile kopyalanıyor, hiç mutate edilmiyor. Davranış değişmedi: kilitli bir OneTimeDelivery, content handler'ı hâlâ tam bir kez çağırıyor; ya indirme bitince ya da sistem serviceExtensionTimeWillExpire'ı çağırdığında.

Ders README'ye yazıldı: CI, macos-26 runner'ının varsayılan Xcode'uyla derliyor, paket ise Xcode 27 ile geliştiriliyor. Yerelde geçmek, desteklenen en eski toolchain'de geçmek demek değil.

Aynı PR iki CI sorununu daha düzeltti. Örnek uygulamanın UI testleri "Timed out while launching application via Xcode" ile zaman aşımına uğruyordu; push testi kendini atlamadan önce uygulamayı başlatıp izin penceresine cevap veriyordu ve ilk açılış soğuk bir simülatörle yarışıyordu (90887f3). Link kontrolü de yalnız merge'den sonra var olacak 331 adreste başarısız oluyordu (5149dfb). Bunlar ancak yeni CI kırmızıya dönebildiği için görüldü: d715e25 eski CI'ın testleri continue-on-error ile koşturduğunu, yani hataların onu hiç kırmızıya çeviremediğini not ediyor.

Kullanım

README'deki hızlı başlangıç, yeni bir iOS uygulaması için eksiksiz bir ContentView.swift. Hem örnek uygulamanın hem swift test'in derlediği ReadmeSamples.swift dosyasından kopyalanmış:

swift
1import NotificationKit
2import SwiftUI
3 
4let scheduler = NotificationScheduler() // one for the whole app
5 
6struct ContentView: View {
7 var body: some View {
8 Button("Remind me in 10 seconds") {
9 Task {
10 do {
11 try await scheduler.requestAuthorization()
12 try await scheduler.schedule(LocalNotification(
13 id: "water",
14 title: "Time for a glass of water",
15 body: "You asked for this reminder 10 seconds ago.",
16 trigger: .after(seconds: 10)
17 ))
18 } catch {
19 print("Could not schedule the reminder:", error)
20 }
21 }
22 }
23 }
24}

Sessiz saatler ve sıklık sınırı scheduler'a verilen bir politika:

swift
1func makeScheduler() throws -> NotificationScheduler {
2 NotificationScheduler(policy: SchedulingPolicy(
3 // One-time notifications that would fire between 22:00 and 07:30 arrive at 07:30.
4 quietHours: try QuietHours(from: "22:00", to: "07:30"),
5 // At most three notifications in any hour; later ones move back.
6 frequencyCap: try FrequencyCap(limit: 3, per: 3600)
7 ))
8}

In-memory center sistemin sınırına sahip olduğu için 64 slot davranışı düz bir unit test'te kontrol edilebilir:

swift
1func scheduledCount() async throws -> Int {
2 let center = InMemoryNotificationCenter(authorizationStatus: .authorized)
3 let scheduler = NotificationScheduler(center: center, store: InMemoryNotificationStore())
4 let reminders = (1...70).map { LocalNotification(id: "r\($0)", title: "Reminder \($0)", trigger: .after(minutes: Double($0))) }
5 try await scheduler.schedule(reminders)
6 return await center.pending.count // 64: the other 6 wait in the scheduler's queue
7}

v2.0.0 tag'inde swift test 109 Swift Testing testi koşturuyor: NotificationKit için 15 suite'te 95, NotificationKitAPNs için 4 suite'te 14.

Sınırlar

  • Kuyruk yalnız uygulama çalışırken ilerler. Planlama üzerine DocC makalesi refresh()'in açılışta ve uygulama ön plana döndüğünde çağrılmasını söylüyor. Kullanıcı uygulamayı açmazsa kuyruktakiler sisteme ulaşmaz; tarihi geçen bildirim düşürülür ve expired olarak raporlanır, ya da overdueBehavior .deliverNow ise geç teslim edilir.
  • Teslim bir çıkarım. Sistemin artık tutmadığı ve tarihi geçmiş tek seferlik bir bildirim teslim edilmiş sayılır.
  • Politika değişikliği geriye işlemez. updatePolicy(_:) dokümanı, planlanmış bildirimlerin tarihlerini koruduğunu yazıyor.
  • Olay akışları sınırlı. NotificationEventHandler varsayılan olarak akış başına okunmamış en yeni 32 olayı tutar; kimsenin okumadığı olaylar düşer.
  • Platformlar farklı. Konum trigger'ları yalnız iOS ve watchOS'ta çalışır; watchOS'ta ek dosya yok ve adlandırılmış sesler yerine varsayılan ses çalar. NotificationKitAPNs CryptoKit'e ihtiyaç duyar ve Linux'ta derlenmez.
  • Testler ağda durur. APNs testleri Apple'ın sunucularına değil bir APNsTransport stub'ına karşı koşar. UI testleri bir banner dokunuşunu, 64'ü aşan kuyruğu ve bir simülatör push'unu kapsar.

README'deki yol haritasında Swift Package Index kaydı, Live Activity'ler için broadcast push kanalları ve NotificationKitAPNs'in swift-crypto ile Linux derlemesi var.

Kaynak kod: github.com/muhittincamdali/iOS-Notification-Framework

Etiketler

#swift#notifications#apns#swift-concurrency#testing#swift-package
Muhittin Çamdalı

Muhittin Çamdalı

Senior Mobile Engineer

12+ yıllık deneyime sahip Senior Mobile Engineer. iOS, Flutter ve React Native ile mobil ürünler geliştiriyorum; full-stack ve AI-native çalışıyorum.

iOS Geliştirme Haberleri

Haftalık Swift tips, SwiftUI tricks ve iOS best practices. Spam yok, sadece değerli içerik.

Onay e-postasındaki bağlantıyı açıp “Aboneliğimi onayla” düğmesine bastığında aboneliğin başlar. Bültende açılma/tıklama istatistikleri tutulur; dilediğin an tek tıkla ayrılabilirsin. Gizlilik

Paylaş

İlgili İçerik