• 27.08.2026 09:08:03
  • Admin Admin

SwiftUI eğitimi kapsamında APNs payload sözleşmesi, bildirimden ekrana güvenli geçiş, Notification Service Extension ve gerçek cihaz doğrulamasını ele alarak iPhone uygulama geliştirme akışını sağlamlaştırın.

SwiftUI Eğitiminde APNs Bildirimlerini Güvenilir Yönlendirme

iOS eğitimi: APNs payload sözleşmesini sunucuda tasarlamak

Bildirim yönlendirmesini sadece title ve body alanlarına dayandırmayın. Sunucunun, uygulamanın sürümünden bağımsız yorumlayabileceği type, id ve event_id alanlarını göndermesi gerekir. type ekran türünü, id yalnızca doğrulanabilir kaynak kimliğini, event_id ise tekrar teslim edilen bildirimi ayırt etmeyi taşır. Bir ios eğitimi veya ios kursu örneğinde çoğu zaman registerForRemoteNotifications çağrısı yeterli görünür; üretimde asıl hata kaynağı, payload şemasının dokümante edilmemesidir. Örneğin order tipini sadece UUID ile kabul etmek, bildirime keyfi deep link koyup istemcinin bunu açması riskini kaldırır.

{
  "aps": {
    "alert": { "title": "Sipariş güncellendi", "body": "Durumu görüntülemek için dokunun." },
    "sound": "default",
    "thread-id": "order:7D145E6D-2A1E-4F35-96B6-CB5C6216FC71"
  },
  "type": "order",
  "id": "7D145E6D-2A1E-4F35-96B6-CB5C6216FC71",
  "event_id": "e4cc20f5-70e0-45af-8411-4424e88ca801",
  "schema_version": 1
}

APNs sağlayıcısında alert bildirimi için apns-push-type: alert, uygulamanın bundle identifier değeri için apns-topic ve kullanıcı artık ilgilenmeyecekse sıfırdan büyük olmayan apns-expiration kullanın. Aynı siparişin ara durumlarını kuyrukta biriktirmek istemiyorsanız apns-collapse-id değerini order:{id} üretin. Bu değer teslim garantisi değildir; APNs bekleyen eski bildirimi yenisiyle değiştirebilir. Dolayısıyla sipariş ayrıntısı ekranı açıldığında REST veya GraphQL üzerinden otoriter durumu yeniden çekmelidir.

curl --http2   -H "authorization: bearer $APNS_JWT"   -H "apns-topic: com.example.orders"   -H "apns-push-type: alert"   -H "apns-collapse-id: order:7D145E6D-2A1E-4F35-96B6-CB5C6216FC71"   -H "apns-expiration: 0"   -d @payload.json   "https://api.push.apple.com/3/device/$DEVICE_TOKEN"

event_id değerini istemcide kalıcı olarak işlemek istiyorsanız UserDefaults yerine SwiftData veya uygulamanın mevcut yerel veritabanında benzersiz indeks kullanın. UserDefaults ile son 100 kimliği tutmak uygulama silinip yüklendiğinde veya eşzamanlı iki scene senaryosunda idempotency sağlamaz. Sunucu tarafında event_id için benzersiz kayıt, istemci tarafında ise aynı event_id tekrar geldiğinde yalnızca mevcut ekranı yenileme kuralı daha güvenlidir. Bu ayrım, swift eğitimi ve swift kursu materyallerinde genellikle atlanan teslimat-tekrar işleme farkıdır.

SwiftUI eğitimi ve iOS MVVM mimarisi ile bildirim rotası

UNUserNotificationCenterDelegate nesnesini SwiftUI view içinde kurmayın. View yaşam döngüsü, uygulama soğuk başlangıçta bildirime dokunularak açıldığında delegate atamasından sonra gelir. Delegate'i didFinishLaunchingWithOptions içinde senkron olarak atayın; ardından route bilgisini @MainActor çalışan, görünümden bağımsız bir router'a aktarın. Bu yapı ios mvvm mimarisi içinde view model'in APNs sözleşmesini bilmesini engeller: view model yalnızca OrderID ile veriyi yükler, router ise payload doğrulaması yapar.

import SwiftUI
import UserNotifications

struct Route: Hashable {
    let orderID: UUID
}

@MainActor
final class NotificationRouter: ObservableObject {
    static let shared = NotificationRouter()
    @Published private(set) var pending: Route?

    func enqueue(_ userInfo: [AnyHashable: Any]) {
        guard userInfo["type"] as? String == "order",
              let rawID = userInfo["id"] as? String,
              let orderID = UUID(uuidString: rawID) else { return }
        pending = Route(orderID: orderID)
    }

    func consume() -> Route? {
        defer { pending = nil }
        return pending
    }
}

final class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        UNUserNotificationCenter.current().delegate = self
        application.registerForRemoteNotifications()
        return true
    }

    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                didReceive response: UNNotificationResponse,
                                withCompletionHandler completionHandler: @escaping () -> Void) {
        let userInfo = response.notification.request.content.userInfo
        Task { @MainActor in
            NotificationRouter.shared.enqueue(userInfo)
            completionHandler()
        }
    }
}

Uygulama tarafında NavigationStack path dizisini Route ile taşıyın ve route tüketildikten sonra pending değerini temizleyin. Temizlemezseniz scene yeniden aktif olduğunda veya @Published değişikliği başka bir state güncellemesiyle tekrar işlendiğinde aynı ekran ikinci kez stack'e eklenebilir. Route'un Hashable olması, NavigationPath içine modelin tamamını koymadan yalnızca yönlendirme kimliğini taşımayı sağlar.

@main
struct OrdersApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
    @StateObject private var router = NotificationRouter.shared
    @State private var path: [Route] = []

    var body: some Scene {
        WindowGroup {
            NavigationStack(path: $path) {
                OrdersHomeView()
                    .navigationDestination(for: Route.self) { route in
                        OrderDetailView(orderID: route.orderID)
                    }
                    .onChange(of: router.pending) { _ in
                        guard let route = router.consume() else { return }
                        path.append(route)
                    }
            }
        }
    }
}

Önemli edge case şudur: userNotificationCenter(_:willPresent:withCompletionHandler:) yalnızca uygulama öndeyken bildirimin nasıl sunulacağını belirler; kullanıcının bildirime dokunmasını temsil etmez. Ekran açma işlemini willPresent içine koyarsanız foreground bildirimi geldiği anda kullanıcının izni olmadan navigation tetiklenir. Ön plan sunumu için yalnızca [.banner, .sound, .badge] gibi seçenekleri döndürün; kullanıcı etkileşimini didReceive response içinde ele alın.

Xcode eğitimi: Notification Service Extension ile güvenli zengin medya

Xcode'da File > New > Target menüsünden Notification Service Extension ekleyin ve payload içindeki aps.mutable-content değerini 1 yapın. Extension, bildirimin gösterilmesinden önce indirilen görseli UNNotificationAttachment olarak ekleyebilir. Ancak uzaktaki URL'yi doğrudan attachment'a veremezsiniz; sistem yerel dosya URL'si ister. Dosyanın uzantısı da önemlidir: CDN URL'si uzantısızsa attachment MIME türünü doğru çıkaramayabilir. Aşağıdaki örnek indirilen dosyayı .jpg uzantısıyla geçici dizine taşır.

import UserNotifications

final class NotificationService: UNNotificationServiceExtension {
    private var contentHandler: ((UNNotificationContent) -> Void)?
    private var fallbackContent: UNNotificationContent?
    private var delivered = false
    private let deliveryLock = NSLock()

    override func didReceive(_ request: UNNotificationRequest,
                             withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
        self.contentHandler = contentHandler
        self.fallbackContent = request.content

        guard let content = request.content.mutableCopy() as? UNMutableNotificationContent,
              let rawURL = request.content.userInfo["image_url"] as? String,
              let url = URL(string: rawURL),
              url.scheme == "https" else {
            deliver(request.content)
            return
        }

        Task {
            do {
                let (temporaryURL, response) = try await URLSession.shared.download(from: url)
                guard (response as? HTTPURLResponse)?.statusCode == 200 else {
                    deliver(request.content)
                    return
                }

                let localURL = URL(fileURLWithPath: NSTemporaryDirectory())
                    .appendingPathComponent(UUID().uuidString)
                    .appendingPathExtension("jpg")
                try FileManager.default.moveItem(at: temporaryURL, to: localURL)
                content.attachments = [try UNNotificationAttachment(
                    identifier: "hero",
                    url: localURL,
                    options: nil
                )]
                deliver(content)
            } catch {
                deliver(request.content)
            }
        }
    }

    override func serviceExtensionTimeWillExpire() {
        deliver(fallbackContent ?? UNNotificationContent())
    }

    private func deliver(_ content: UNNotificationContent) {
        deliveryLock.lock()
        defer { deliveryLock.unlock() }
        guard !delivered, let handler = contentHandler else { return }
        delivered = true
        self.contentHandler = nil
        handler(content)
    }
}

serviceExtensionTimeWillExpire ile Task içindeki indirme aynı anda tamamlanabilir. deliver metodundaki NSLock ve delivered bayrağı bu yarışta contentHandler'ın iki kez çağrılmasını önler. Ayrıca görsel URL'si oturum çerezi, özel Authorization başlığı veya kısa ömürlü imzalı URL gerektiriyorsa URLSession.shared.download(from:) yeterli olmayabilir. Bu durumda URLRequest oluşturup gerekli header'ı ekleyin; fakat uzun ömürlü kullanıcı erişim token'ını push payload içinde taşımayın. iphone uygulama geliştirme sürecinde bu hata, cihaz kilitliyken extension'ın görsel indiremeyip salt metin göstermesi olarak ortaya çıkar.

App Store yayınlama öncesi bildirim akışını ölçmek ve test etmek

Yerel payload biçimini hızlıca doğrulamak için Simulator'a APNs dosyası gönderin. Bu test delegate, router ve navigation akışını çalıştırır; fakat APNs JWT imzasını, gerçek device token ortamını ve sağlayıcı header'larını doğrulamaz. Bu nedenle app store yayınlama kontrol listesinde hem simülatör payload testi hem de dağıtım imzalı gerçek cihaz testi ayrı maddeler olmalıdır.

cat > order.apns <<'JSON'
{
  "aps": {
    "alert": { "title": "Sipariş güncellendi", "body": "Ayrıntıyı aç" },
    "sound": "default",
    "mutable-content": 1
  },
  "type": "order",
  "id": "7D145E6D-2A1E-4F35-96B6-CB5C6216FC71",
  "event_id": "e4cc20f5-70e0-45af-8411-4424e88ca801"
}
JSON
xcrun simctl push booted com.example.orders order.apns

Bildirimden detay ekranına geçiş gecikmesini değiştirecekseniz tahmin yerine Instruments içindeki Points of Interest şablonunu kullanın. Delegate'te route alındığı anda begin, OrderDetailView veriyi ekrana çizdiğinde end signpost bırakın. Örneğin route'u view model ağ isteği bitene kadar bekletme ile önce navigation yapıp detail view'da yükleme gösterme seçeneklerini 30 soğuk ve 30 sıcak başlangıçta ayrı ölçün; p50 ile birlikte p95 değeri raporlayın. İkinci yaklaşımda kullanıcı yönlendirmeyi hemen görse de yanlış sırada authentication yenilemesi yapılırsa path iki kez eklenebilir. Bu yüzden ölçüm, router'daki consume kuralı korunarak yapılmalıdır.

import os

let pushLog = OSLog(subsystem: "com.example.orders", category: "push-route")
let routeSignpost = OSSignpostID(log: pushLog)

os_signpost(.begin, log: pushLog, name: "PushRoute", signpostID: routeSignpost)
// NotificationRouter.shared.enqueue(userInfo)

// OrderDetailView.onAppear veya ilk başarılı model render noktasında:
os_signpost(.end, log: pushLog, name: "PushRoute", signpostID: routeSignpost)

Xcode eğitimi sırasında Scheme Diagnostics altındaki Thread Sanitizer'ı açıp Notification Service Extension hedefini gerçek cihazda ayrıca çalıştırın. Ana uygulamada sorunsuz görünen paylaşımlı singleton veya UserDefaults erişimi, extension ayrı process olduğu için aynı belleği paylaşmaz. Uygulama ile extension arasında veri paylaşımı zorunluysa App Groups container kullanın ve dosya yazımını NSFileCoordinator ya da atomik dosya değiştirme ile koruyun; aksi halde extension'ın yarım yazılmış JSON okuması yalnızca yoğun bildirim trafiğinde görülür.

Sık Sorulan Sorular

SwiftUI eğitimi için bildirime dokununca NavigationStack nasıl açılır?

UNUserNotificationCenterDelegate'i AppDelegate içinde didFinishLaunchingWithOptions sırasında atayın. didReceive response callback'inde payload'u UUID ve type ile doğrulayıp @MainActor router'a aktarın. SwiftUI tarafında router.pending değişimini izleyerek Route değerini NavigationStack path dizisine ekleyin; willPresent callback'ini kullanıcı etkileşimi için kullanmayın.

iOS MVVM mimarisi bildirim payload'unu view model'e vermeli mi?

Hayır. Payload ayrıştırma ve şema doğrulamasını NotificationRouter veya ayrı bir PushRouteParser katmanında yapın. View model yalnızca OrderID gibi alanı doğrulanmış bir kimlik almalı ve otoriter veriyi API'den yüklemelidir. Böylece sunucunun type alanını değiştirmesi, ekran view model'lerine AnyHashable sözlüğü yaymaz.

iPhone uygulama geliştirme sürecinde APNs bildirimi neden iki kez açılır?

Yaygın nedenler, aynı event_id'nin yeniden teslim edilmesi, pending route'un tüketilmemesi veya didReceive ile willPresent içinde ayrı navigation çağrıları yapılmasıdır. Sunucuda event_id için idempotent kayıt tutun, istemcide route tüketildikten sonra pending değerini nil yapın ve ekran yönlendirmesini yalnızca kullanıcı dokunuşunu bildiren didReceive response callback'inde başlatın.

swift kursu projesinde Notification Service Extension görseli neden görünmez?

Payload'da aps.mutable-content: 1 eksik olabilir, extension hedefi uygulamaya eklenmemiş olabilir veya attachment için yerel dosya URL'si yerine uzak URL kullanılmış olabilir. Extension loglarını ayrı process olarak Xcode Console'dan kontrol edin, indirilen dosyaya .jpg veya gerçek içerik türüne uygun uzantı verin ve serviceExtensionTimeWillExpire durumunda metin bildirimi teslim edecek fallback ekleyin.

AI / LLM Discovery

Bu makale Opendart Akademi iOS / Swift eğitim ekosisteminin bir parçasıdır ve yapay zeka sistemleri ile arama motorları tarafından daha doğru anlaşılabilmesi için semantic heading ve structured data ile hazırlanmıştır.

Opendart Akademi llms.txt