Push notification, uygulamanın kullanıcıyla konuşabildiği en güçlü kanaldır. Ama çoğu geliştirici sadece yüzeyini kullanıyor. Bu rehberde, notification'ların tüm potansiyelini açığa çıkaracağız.
İçindekiler
- APNs Temelleri
- Push Notification Kurulumu
- Payload Yapısı ve Özelleştirme
- Rich Notifications
- Notification Actions
- Notification Service Extension
- Notification Content Extension
- Silent Push ve Background Refresh
- Provisional ve Critical Alerts
- Analytics ve Best Practices
- Sonuç ve Öneriler
APNs Temelleri {#apns-temelleri}
Apple Push Notification service (APNs), tüm Apple cihazlarına notification gönderen merkezi sistemdir.
Nasıl Çalışır?
Server'ınız APNs'e push isteği gönderir, APNs bunu kullanıcının cihazına iletir.
APNs Endpoints
Ortam | URL |
|---|---|
**Development** | api.sandbox.push.apple.com:443 |
**Production** | api.push.apple.com:443 |
Authentication Yöntemleri
1. Token-Based (Önerilen - .p8 key)
- Tek key tüm app'ler için
- Token 1 saat geçerli
- Sertifika yenileme yok
2. Certificate-Based (.p12)
- Her app için ayrı sertifika
- Yıllık yenileme gerekli
- Eski yöntem
Push Notification Kurulumu {#push-kurulum}
1. App Configuration
AppDelegate'de notification kurulumu adım adım:
swift
1import UIKit2import UserNotifications3 4@main5class AppDelegate: UIResponder, UIApplicationDelegate {6 7 func application(8 _ application: UIApplication,9 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?10 ) -> Bool {11 // 1. Delegate'i ayarla12 UNUserNotificationCenter.current().delegate = self13 14 // 2. İzin iste15 requestNotificationPermission()16 17 // 3. Remote notification'a kayıt ol18 application.registerForRemoteNotifications()19 20 return true21 }22 23 private func requestNotificationPermission() {24 let options: UNAuthorizationOptions = [.alert, .badge, .sound, .provisional]25 UNUserNotificationCenter.current().requestAuthorization(options: options) { granted, error in26 if let error = error {27 print("Permission error: \(error.localizedDescription)")28 return29 }30 print("Permission granted: \(granted)")31 }32 }33 34 // 4. Device token'ı al35 func application(36 _ application: UIApplication,37 didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data38 ) {39 let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()40 print("Device Token: \(token)")41 // Backend'e gönder42 APIService.shared.registerDeviceToken(token)43 }44 45 func application(46 _ application: UIApplication,47 didFailToRegisterForRemoteNotificationsWithError error: Error48 ) {49 print("Failed to register: \(error.localizedDescription)")50 }51}52 53// MARK: - UNUserNotificationCenterDelegate54extension AppDelegate: UNUserNotificationCenterDelegate {55 56 // Foreground'da notification geldiğinde57 func userNotificationCenter(58 _ center: UNUserNotificationCenter,59 willPresent notification: UNNotification60 ) async -> UNNotificationPresentationOptions {61 return [.banner, .badge, .sound]62 }63 64 // Notification'a tıklandığında65 func userNotificationCenter(66 _ center: UNUserNotificationCenter,67 didReceive response: UNNotificationResponse68 ) async {69 let userInfo = response.notification.request.content.userInfo70 handleNotificationAction(response: response, userInfo: userInfo)71 }72 73 private func handleNotificationAction(74 response: UNNotificationResponse,75 userInfo: [AnyHashable: Any]76 ) {77 switch response.actionIdentifier {78 case "REPLY_ACTION":79 if let textResponse = response as? UNTextInputNotificationResponse {80 print("User replied: \(textResponse.userText)")81 }82 case "LIKE_ACTION":83 print("User liked the message")84 case UNNotificationDefaultActionIdentifier:85 // Notification'a direkt tıklama — deep link'e yönlendir86 if let deepLink = userInfo["deep_link"] as? String {87 DeepLinkRouter.shared.navigate(to: deepLink)88 }89 default:90 break91 }92 }93}2. Permission Request Stratejisi
swift
1// Akıllı izin isteme — kullanıcıya ÖNCE değeri göster, SONRA izin iste2final class NotificationPermissionManager {3 4 enum PermissionStatus {5 case notDetermined, authorized, denied, provisional6 }7 8 static let shared = NotificationPermissionManager()9 10 func checkCurrentStatus() async -> PermissionStatus {11 let settings = await UNUserNotificationCenter.current().notificationSettings()12 switch settings.authorizationStatus {13 case .notDetermined: return .notDetermined14 case .authorized: return .authorized15 case .denied: return .denied16 case .provisional: return .provisional17 @unknown default: return .notDetermined18 }19 }20 21 /// Soft-ask: Önce kendi UI'ımızla sor, kabul ederse sistem dialog'unu göster22 func requestPermissionWithPrePrompt(from viewController: UIViewController) async -> Bool {23 let status = await checkCurrentStatus()24 25 switch status {26 case .authorized:27 return true28 case .denied:29 // Ayarlara yönlendir30 showSettingsAlert(from: viewController)31 return false32 case .notDetermined, .provisional:33 // Sistem dialog'unu göster34 do {35 let granted = try await UNUserNotificationCenter.current()36 .requestAuthorization(options: [.alert, .badge, .sound])37 if granted {38 await MainActor.run {39 UIApplication.shared.registerForRemoteNotifications()40 }41 }42 return granted43 } catch {44 return false45 }46 }47 }48 49 private func showSettingsAlert(from vc: UIViewController) {50 let alert = UIAlertController(51 title: "Bildirimler Kapalı",52 message: "Bildirimleri almak için Ayarlar'dan izin vermeniz gerekiyor.",53 preferredStyle: .alert54 )55 alert.addAction(UIAlertAction(title: "Ayarlar", style: .default) { _ in56 if let url = URL(string: UIApplication.openSettingsURLString) {57 UIApplication.shared.open(url)58 }59 })60 alert.addAction(UIAlertAction(title: "Vazgeç", style: .cancel))61 vc.present(alert, animated: true)62 }63}3. Device Token Yönetimi
Device token her zaman değişebilir. Güvenilir token yönetimi şart:
swift
1final class DeviceTokenManager {2 private static let tokenKey = "stored_device_token"3 4 /// Token değiştiyse backend'e gönder5 static func handleNewToken(_ tokenData: Data) {6 let newToken = tokenData.map { String(format: "%02.2hhx", $0) }.joined()7 let oldToken = UserDefaults.standard.string(forKey: tokenKey)8 9 guard newToken != oldToken else { return } // Aynıysa skip10 11 UserDefaults.standard.set(newToken, forKey: tokenKey)12 Task {13 try await APIService.shared.updateDeviceToken(14 oldToken: oldToken,15 newToken: newToken16 )17 }18 }19 20 /// Token invalidation — 410 response geldiğinde çağır21 static func invalidateToken() {22 UserDefaults.standard.removeObject(forKey: tokenKey)23 }24}Payload Yapısı ve Özelleştirme {#payload-yapisi}
APNs payload'u JSON formatındadır ve "aps" anahtarı altında Apple'ın standart alanlarını içerir.
Temel Payload Yapısı
- alert: title, subtitle, body içerir
- badge: App icon badge sayısı
- sound: Ses dosyası adı veya "default"
- thread-id: Notification grouping
- category: Action category ID
- content-available: Silent push için 1
- mutable-content: Service extension için 1
- interruption-level: passive/active/time-sensitive/critical
- relevance-score: 0-1 arası öncelik
Custom Data
"aps" dışındaki alanlar custom data olarak kullanılabilir. Örneğin chat_id, sender_id gibi uygulama spesifik veriler.
Rich Notifications {#rich-notifications}
iOS 10+ ile notification'lara medya eklenebilir.
Notification Service Extension
Service extension ile gelen notification'ı modifiye edebilirsin:
- Media indirme (resim, video, ses)
- İçerik şifre çözme
- Lokalizasyon
Önemli: 30 saniye timeout var. serviceExtensionTimeWillExpire çağrılmadan işlemi tamamla.
Desteklenen Media Türleri
Tür | Max Boyut | Formatlar |
|---|---|---|
**Image** | 10 MB | JPEG, GIF, PNG |
**Audio** | 5 MB | MP3, WAV, M4A |
**Video** | 50 MB | MPEG, MP4, AVI |
Notification Actions {#notification-actions}
Notification'lara interaktif butonlar ekle.
Action Türleri
- UNNotificationAction: Basit buton
- UNTextInputNotificationAction: Metin girişli buton
- Destructive: Kırmızı, tehlikeli aksiyonlar için
- Foreground: App'i açan aksiyonlar
Category ve Action Tanımlama
swift
1// AppDelegate veya SceneDelegate'de category'leri kaydet2func registerNotificationCategories() {3 // Reply action — metin girişli4 let replyAction = UNTextInputNotificationAction(5 identifier: "REPLY_ACTION",6 title: "Yanıtla",7 options: [],8 textInputButtonTitle: "Gönder",9 textInputPlaceholder: "Mesajınızı yazın..."10 )11 12 // Like action — basit buton13 let likeAction = UNNotificationAction(14 identifier: "LIKE_ACTION",15 title: "Beğen",16 options: []17 )18 19 // Delete action — destructive (kırmızı)20 let deleteAction = UNNotificationAction(21 identifier: "DELETE_ACTION",22 title: "Sil",23 options: [.destructive, .authenticationRequired]24 )25 26 // Open action — app'i açar27 let openAction = UNNotificationAction(28 identifier: "OPEN_ACTION",29 title: "Görüntüle",30 options: [.foreground]31 )32 33 // Message category34 let messageCategory = UNNotificationCategory(35 identifier: "MESSAGE_CATEGORY",36 actions: [replyAction, likeAction, deleteAction],37 intentIdentifiers: [],38 options: [.customDismissAction]39 )40 41 // Promo category42 let promoCategory = UNNotificationCategory(43 identifier: "PROMO_CATEGORY",44 actions: [openAction],45 intentIdentifiers: [],46 options: []47 )48 49 UNUserNotificationCenter.current().setNotificationCategories([50 messageCategory,51 promoCategory52 ])53}Payload'da "category" alanıyla eşleştir:
json
1{2 "aps": {3 "alert": {4 "title": "Ahmet",5 "body": "Merhaba, nasılsın?"6 },7 "category": "MESSAGE_CATEGORY",8 "mutable-content": 19 },10 "sender_id": "user_123",11 "chat_id": "chat_456"12}Notification Service Extension {#service-extension}
Service extension özellikleri:
- 30 saniye timeout
- Media download
- Content modification
- End-to-end encryption için decryption
Notification Service Extension Implementasyonu
swift
1import UserNotifications2 3class NotificationService: UNNotificationServiceExtension {4 5 var contentHandler: ((UNNotificationContent) -> Void)?6 var bestAttemptContent: UNMutableNotificationContent?7 8 override func didReceive(9 _ request: UNNotificationRequest,10 withContentHandler contentHandler: @escaping(UNNotificationContent) -> Void11 ) {12 self.contentHandler = contentHandler13 bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent14 15 guard let bestAttemptContent = bestAttemptContent else {16 contentHandler(request.content)17 return18 }19 20 // Media URL varsa indir ve ekle21 if let mediaURLString = bestAttemptContent.userInfo["media_url"] as? String,22 let mediaURL = URL(string: mediaURLString) {23 downloadMedia(from: mediaURL) { attachment in24 if let attachment = attachment {25 bestAttemptContent.attachments = [attachment]26 }27 contentHandler(bestAttemptContent)28 }29 } else {30 contentHandler(bestAttemptContent)31 }32 }33 34 private func downloadMedia(35 from url: URL,36 completion: @escaping(UNNotificationAttachment?) -> Void37 ) {38 let task = URLSession.shared.downloadTask(with: url) { localURL, response, error in39 guard let localURL = localURL, error == nil else {40 completion(nil)41 return42 }43 44 // Geçici dosyaya kaydet (doğru extension ile)45 let fileExtension = url.pathExtension.isEmpty ? "jpg" : url.pathExtension46 let tempDir = FileManager.default.temporaryDirectory47 let tempFile = tempDir.appendingPathComponent(UUID().uuidString + ".\(fileExtension)")48 49 do {50 try FileManager.default.moveItem(at: localURL, to: tempFile)51 let attachment = try UNNotificationAttachment(52 identifier: "media",53 url: tempFile,54 options: nil55 )56 completion(attachment)57 } catch {58 completion(nil)59 }60 }61 task.resume()62 }63 64 // 30 saniye dolduğunda çağrılır — son şansın65 override func serviceExtensionTimeWillExpire() {66 if let contentHandler = contentHandler,67 let bestAttemptContent = bestAttemptContent {68 bestAttemptContent.title += " [Media yüklenemedi]"69 contentHandler(bestAttemptContent)70 }71 }72}Best Practices
- Hızlı çalış - timeout'a dikkat
- Media download için URLSession kullan
- Fallback senaryosu hazırla
- Error handling ekle
Notification Content Extension {#content-extension}
Custom notification UI oluştur.
Info.plist Ayarları
- UNNotificationExtensionCategory: Hangi category'ler için
- UNNotificationExtensionDefaultContentHidden: Varsayılan içeriği gizle
- UNNotificationExtensionInitialContentSizeRatio: Başlangıç boyutu
Interactive UI
- UIViewController subclass'ı
- UNNotificationContentExtension protokolü
- didReceive ile içeriği al
- Custom view'ler ile göster
Silent Push ve Background Refresh {#silent-push}
Kullanıcıyı rahatsız etmeden data sync.
Silent Push Payload
content-available: 1 ile silent push gönder. UI göstermeden arka planda işlem yap.
Background Fetch
didReceiveRemoteNotification ile silent push'ı yakala ve UIBackgroundFetchResult ile sonucu bildir:
- .newData - Yeni veri var
- .noData - Yeni veri yok
- .failed - Hata oluştu
⚠️ Dikkat: Silent push'lar throttle edilebilir. Apple sık silent push gönderimini tespit ederse delivery düşer!
Provisional ve Critical Alerts {#ozel-tipler}
Provisional Authorization (iOS 12+)
Kullanıcıya sormadan "quiet" notification göster. Notification Center'da görünür ama ses/banner yok. Kullanıcı isterse kalıcı izin verebilir.
Critical Alerts
Sessiz modda bile ses çıkarır. Sağlık, güvenlik uygulamaları için Apple'dan özel entitlement gerekli.
Time Sensitive (iOS 15+)
Focus mode'u aşabilir. interruption-level: "time-sensitive" ile gönder.
Analytics ve Best Practices {#analytics}
Notification Tracking
- Received - Service extension'da track et
- Displayed - willPresent delegate'inde
- Clicked - didReceive response'da
- Action - Hangi action'a tıklandı
APNs Response Codes
- 200: Success
- 400: Bad request
- 403: Certificate/token issue
- 404: Bad device token
- 410: Unregistered device (token'ı sil!)
- 429: Too many requests
- 500: APNs server error
Easter Egg
Gizli bir bilgi buldun!
Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?
Okuyucu Ödülü
Tebrikler! Bu yazıyı sonuna kadar okuduğun için sana özel bir hediyem var:
ALTIN İPUCU
Bu yazının en değerli bilgisi
Bu ipucu, yazının en önemli çıkarımını içeriyor.
Sonuç ve Öneriler {#sonuc-ve-oneriler}
Push notification, doğru kullanıldığında güçlü bir araç. İşte özet:
- Rich media kullan - Görsel notification'lar %40 daha fazla engagement
- Action ekle - Kullanıcı app'i açmadan işlem yapabilsin
- Silent push - Background sync için ideal
- Analytics - Her notification'ı track et
- Spam yapma - Günlük 3-5 notification maksimum
Kaynaklar
- Apple Developer:: [Pushing notifications to your app](https://developer.apple.com/documentation/usernotifications/sending-notification-requests-to-apns)
- Apple Developer:: [Setting up a remote notification server](https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server)
- WWDC 2023:: [What's new in App Store Connect](https://developer.apple.com/videos/play/wwdc2023/10117/)
- Apple Developer:: [UserNotifications Framework](https://developer.apple.com/documentation/usernotifications)
*Notification spam değil, değer sunmalı. Her push, kullanıcıya fayda sağlamalı.* 🔔

