Skip to content

Swift Notifications — Local, Remote, and In-App NotificationCenter

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about Swift Notifications. We cover key concepts, practical examples, and best practices to help you master this topic.

Swift notifications encompass both in-app event broadcasting via NotificationCenter and user-facing alerts through UNUserNotificationCenter that can display banners, play sounds, and update badge counts even when the app is closed.

What You'll Learn

  • NotificationCenter for in-app event communication
  • UNUserNotificationCenter for local notifications
  • Remote push notifications setup and handling
  • Notification actions and categories
  • Permission requests and notification management
  • Testing notifications on simulator and device

Why It Matters

Notifications are essential for user engagement and real-time updates. Local notifications remind users of appointments, push notifications deliver breaking news, and in-app notifications coordinate between components. iOS's notification system requires careful permission handling and respects user privacy choices.

Real-World Use

A calendar app sends local notifications for upcoming events, receives push notifications when a meeting is cancelled, uses NotificationCenter to update the UI when data changes, and provides interactive notification actions to snooze or accept meeting invitations directly from the banner.

Learning Path

flowchart LR
  A[Concurrency
Lesson 22] --> B[Notifications
You are here] B --> C[Combine Framework
Lesson 24] B --> D[SwiftUI Basics
Lesson 25] style B fill:#f90,color:#fff

NotificationCenter (In-App Events)

NotificationCenter enables loosely coupled communication between components within the same app Process.

import Foundation

extension Notification.Name {
    static let userDidLogin = Notification.Name("userDidLogin")
    static let userDidLogout = Notification.Name("userDidLogout")
    static let dataDidUpdate = Notification.Name("dataDidUpdate")
}

class NotificationService {
    static let shared = NotificationService()
    private let center = NotificationCenter.default

    func postLogin(username: String) {
        center.post(
            name: .userDidLogin,
            object: nil,
            userInfo: ["username": username, "timestamp": Date()]
        )
    }

    func postLogout() {
        center.post(name: .userDidLogout, object: nil)
    }

    func postDataUpdate() {
        center.post(name: .dataDidUpdate, object: nil)
    }
}

class ProfileViewController {
    private var observers: [NSObjectProtocol] = []

    init() {
        let loginObserver = NotificationCenter.default.addObserver(
            forName: .userDidLogin,
            object: nil,
            queue: .main
        ) { notification in
            if let username = notification.userInfo?["username"] as? String {
                print("Profile: User \(username) logged in")
            }
        }
        observers.append(loginObserver)

        let logoutObserver = NotificationCenter.default.addObserver(
            forName: .userDidLogout,
            object: nil,
            queue: .main
        ) { _ in
            print("Profile: User logged out — clearing UI")
        }
        observers.append(logoutObserver)
    }

    deinit {
        for observer in observers {
            NotificationCenter.default.removeObserver(observer)
        }
    }
}

let profile = ProfileViewController()
NotificationService.shared.postLogin(username: "Alice")
NotificationService.shared.postLogout()

Output:

Profile: User logged in
Profile: User logged out — clearing UI

Always remove observers in deinit to prevent crashes from dangling Observer references.

Local Notifications (UNUserNotificationCenter)

Local notifications are scheduled by the app and delivered by iOS, even when the app is in the background.

import UserNotifications

class LocalNotificationManager {
    static let shared = LocalNotificationManager()
    private let center = UNUserNotificationCenter.current()

    func requestPermission() {
        center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
            if let error = error {
                print("Permission error: \(error.localizedDescription)")
                return
            }
            print("Notification permission \(granted ? "granted" : "denied")")
        }
    }

    func scheduleReminder(title: String, body: String, secondsFromNow: TimeInterval) {
        let content = UNMutableNotificationContent()
        content.title = title
        content.body = body
        content.sound = .default
        content.badge = 1

        let trigger = UNTimeIntervalNotificationTrigger(
            timeInterval: secondsFromNow,
            repeats: false
        )

        let request = UNNotificationRequest(
            identifier: UUID().uuidString,
            content: content,
            trigger: trigger
        )

        center.add(request) { error in
            if let error = error {
                print("Failed to schedule: \(error.localizedDescription)")
            } else {
                print("Notification scheduled in \(Int(secondsFromNow))s: \(title)")
            }
        }
    }

    func scheduleDailyReminder(title: String, body: String, hour: Int, minute: Int) {
        var dateComponents = DateComponents()
        dateComponents.hour = hour
        dateComponents.minute = minute

        let content = UNMutableNotificationContent()
        content.title = title
        content.body = body
        content.sound = .default

        let trigger = UNCalendarNotificationTrigger(
            dateMatching: dateComponents,
            repeats: true
        )

        let request = UNNotificationRequest(
            identifier: "daily_reminder",
            content: content,
            trigger: trigger
        )

        center.add(request) { error in
            if let error = error {
                print("Failed to schedule daily: \(error.localizedDescription)")
            } else {
                print("Daily reminder set for \(hour):\(minute)")
            }
        }
    }

    func removeAllPending() {
        center.removeAllPendingNotificationRequests()
        center.removeAllDeliveredNotifications()
        print("All notifications removed")
    }

    func getPendingCount() {
        center.getPendingNotificationRequests { requests in
            print("Pending notifications: \(requests.count)")
        }
    }
}

let notifManager = LocalNotificationManager.shared
notifManager.requestPermission()
notifManager.scheduleReminder(
    title: "Coffee Break",
    body: "Time to take a break and grab some coffee!",
    secondsFromNow: 10
)
notifManager.scheduleDailyReminder(
    title: "Good Morning",
    body: "Start your day with a smile!",
    hour: 8,
    minute: 0
)

Handling Notification Responses

When the user taps a notification, your app handles the response.

import UIKit

class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate {

    func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        willPresent notification: UNNotification,
        withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
    ) {
        // Show banner even when app is in foreground
        completionHandler([.banner, .sound, .badge])
    }

    func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        didReceive response: UNNotificationResponse,
        withCompletionHandler completionHandler: @escaping () -> Void
    ) {
        let userInfo = response.notification.request.content.userInfo
        let actionIdentifier = response.actionIdentifier

        switch actionIdentifier {
        case UNNotificationDefaultActionIdentifier:
            print("App opened from notification")
        case "SNOOZE_ACTION":
            print("Snooze action selected")
        case "COMPLETE_ACTION":
            print("Complete action selected")
        default:
            print("Unknown action: \(actionIdentifier)")
        }

        if let customData = userInfo["customData"] as? String {
            print("Custom data: \(customData)")
        }

        completionHandler()
    }
}

// Setup in AppDelegate:
// let delegate = NotificationDelegate()
// UNUserNotificationCenter.current().delegate = delegate

Notification Actions

Add interactive buttons to notifications using UNNotificationCategory.

func registerActions() {
    let snoozeAction = UNNotificationAction(
        identifier: "SNOOZE_ACTION",
        title: "Snooze 5 min",
        options: [.authenticationRequired]
    )

    let completeAction = UNNotificationAction(
        identifier: "COMPLETE_ACTION",
        title: "Complete",
        options: [.foreground]
    )

    let category = UNNotificationCategory(
        identifier: "TASK_REMINDER",
        actions: [snoozeAction, completeAction],
        intentIdentifiers: [],
        options: [.customDismissAction]
    )

    UNUserNotificationCenter.current()
        .setNotificationCategories([category])
    print("Notification actions registered")
}

func scheduleActionableReminder() {
    let content = UNMutableNotificationContent()
    content.title = "Task Reminder"
    content.body = "Review the quarterly report"
    content.categoryIdentifier = "TASK_REMINDER"
    content.userInfo = ["taskID": "12345"]

    let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 10, repeats: false)
    let request = UNNotificationRequest(
        identifier: "task_reminder",
        content: content,
        trigger: trigger
    )

    UNUserNotificationCenter.current().add(request) { error in
        if let error = error {
            print("Failed: \(error.localizedDescription)")
        } else {
            print("Actionable reminder scheduled")
        }
    }
}

registerActions()
scheduleActionableReminder()

Remote Push Notifications

Remote notifications are sent from a server to Apple Push Notification Service (APNS), which delivers them to the device.

import UIKit

class PushNotificationManager: NSObject, UNUserNotificationCenterDelegate {

    func registerForPushNotifications() {
        UNUserNotificationCenter.current().delegate = self
        UNUserNotificationCenter.current().requestAuthorization(
            options: [.alert, .sound, .badge]
        ) { granted, _ in
            guard granted else {
                print("Push notification permission denied")
                return
            }
            DispatchQueue.main.async {
                UIApplication.shared.registerForRemoteNotifications()
            }
        }
    }

    func application(
        _ application: UIApplication,
        didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
    ) {
        let tokenParts = deviceToken.map { String(format: "%02.2hhx", $0) }
        let token = tokenParts.joined()
        print("Device token: \(token)")
        // Send token to your server
    }

    func application(
        _ application: UIApplication,
        didFailToRegisterForRemoteNotificationsWithError error: Error
    ) {
        print("Push registration failed: \(error.localizedDescription)")
    }

    func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        didReceive response: UNNotificationResponse,
        withCompletionHandler completionHandler: @escaping () -> Void
    ) {
        let userInfo = response.notification.request.content.userInfo
        // Handle push notification tap
        if let aps = userInfo["aps"] as? [String: Any] {
            print("Push notification tapped: \(aps)")
        }
        completionHandler()
    }
}

// In AppDelegate:
// let pushManager = PushNotificationManager()
// pushManager.registerForPushNotifications()

The device token is unique to the app and device. Send it to your server so it can send targeted push notifications through APNS.

Notification Best Practices

class NotificationBestPractices {

    // 1. Check notification settings before scheduling
    func checkSettings() {
        UNUserNotificationCenter.current().getNotificationSettings { settings in
            switch settings.authorizationStatus {
            case .authorized, .provisional:
                print("Notifications authorized")
            case .denied:
                print("Notifications denied — prompt user in settings")
            case .notDetermined:
                print("Not yet asked")
            case .ephemeral:
                print("Ephemeral authorization")
            @unknown default:
                break
            }
        }
    }

    // 2. Use threadIdentifier for grouping
    func scheduleGroupedNotification() {
        let content = UNMutableNotificationContent()
        content.title = "New message from Alice"
        content.body = "Hey, are you free for lunch?"
        content.threadIdentifier = "chat_12345"

        let request = UNNotificationRequest(
            identifier: UUID().uuidString,
            content: content,
            trigger: nil
        )
        UNUserNotificationCenter.current().add(request) { _ in
            print("Grouped notification added")
        }
    }

    // 3. Implement quiet time
    func scheduleWithQuietTime() {
        let hour = Calendar.current.component(.hour, from: Date())
        guard hour >= 8 && hour < 22 else {
            print("Quiet hours — notification suppressed")
            return
        }
        print("Scheduling notification...")
    }
}

Common Mistakes

  1. Not requesting permission before scheduling: Notifications fail silently if permission is denied. Always check authorizationStatus before scheduling.

  2. Forgetting to set the notification delegate: Without setting UNUserNotificationCenter.current().delegate, notifications do not show banners when the app is in the foreground.

  3. Over-scheduling notifications: iOS limits pending notifications to 64 per app. Exceeding this silently drops requests. Manage pending requests carefully.

  4. Using NotificationCenter without removing observers: This causes crashes when deallocated objects receive notifications. Always remove observers in deinit.

  5. Ignoring the sandbox vs production APNS environment: Development builds use the sandbox APNS environment, TestFlight and App Store builds use production. Server configuration must match.

Practice Questions

  1. What is the difference between NotificationCenter and UNUserNotificationCenter?
  2. How many pending notifications can an iOS app schedule simultaneously?
  3. Why must you remove NotificationCenter observers in deinit?
  4. What is the role of threadIdentifier in notification grouping?
  5. Challenge: Build a medication reminder app that schedules daily notifications at configurable times, includes "Taken" and "Snooze" action buttons, groups notifications by medication type, and tracks which doses were taken.

Mini Project

Create a TaskReminderApp with:

  • A Reminder struct with id, title, date, priority (high, medium, low)
  • Local notification scheduling for each reminder
  • Notification actions: "Complete" (marks done), "Snooze 10 min" (reschedules)
  • Category registration with appropriate actions
  • A delegate that handles foreground presentation and action responses
  • Pending notification count display

FAQ

Can I schedule notifications from a background extension?

Yes, but notification content extensions have limited execution time. Use the main app for complex scheduling logic.

How do I handle notification delivery when the app is killed?

If the app is not running, iOS displays the notification normally. When the user taps it, the app launches and the delegate receives the response.

What are provisional notifications?

Provisional notifications (iOS 12+) bypass the permission dialog. They deliver quietly to Notification Center without sound or alert. Users can later upgrade or deny.

Can I update a delivered notification?

Yes. Use the same identifier when scheduling an updated notification request. It replaces the previous one.

How do I test push notifications without a server?

Use the simctl command line tool or a third-party tool like Pusher to send test pushes to the iOS simulator (iOS 16+) or a real device.

What's Next

After mastering notifications, explore Reactive Programming with Combine Framework for event streams, or start building modern UIs with SwiftUI Basics.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro