0
0
iOS Swiftmobile~20 mins

Push notifications (APNs + FCM) in iOS Swift - Mini App: Build & Ship

Choose your learning style9 modes available
Build: Push Notification Setup
This screen registers the app for push notifications using Apple Push Notification service (APNs) and Firebase Cloud Messaging (FCM). It shows the registration status and the device token once registered.
Target UI
----------------------------------
| Push Notification Setup         |
----------------------------------
| Status: Not Registered          |
|                                |
| Device Token:                   |
|                                |
| [Register for Notifications]   |
----------------------------------
Add a button labeled 'Register for Notifications'.
When tapped, request user permission for notifications.
Register with APNs to get the device token.
Integrate Firebase Messaging to receive FCM token.
Display registration status: 'Not Registered', 'Permission Denied', or 'Registered'.
Show the device token or FCM token on screen after successful registration.
Handle errors gracefully and update status accordingly.
Starter Code
iOS Swift
import UIKit
import UserNotifications
import FirebaseCore
import FirebaseMessaging

class PushNotificationViewController: UIViewController {

    private let statusLabel = UILabel()
    private let tokenLabel = UILabel()
    private let registerButton = UIButton(type: .system)

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        setupUI()
        // TODO: Configure Firebase here
        // TODO: Set Messaging delegate
    }

    private func setupUI() {
        statusLabel.text = "Status: Not Registered"
        statusLabel.translatesAutoresizingMaskIntoConstraints = false
        tokenLabel.numberOfLines = 0
        tokenLabel.translatesAutoresizingMaskIntoConstraints = false
        registerButton.setTitle("Register for Notifications", for: .normal)
        registerButton.translatesAutoresizingMaskIntoConstraints = false
        registerButton.addTarget(self, action: #selector(registerTapped), for: .touchUpInside)

        view.addSubview(statusLabel)
        view.addSubview(tokenLabel)
        view.addSubview(registerButton)

        NSLayoutConstraint.activate([
            statusLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40),
            statusLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            tokenLabel.topAnchor.constraint(equalTo: statusLabel.bottomAnchor, constant: 20),
            tokenLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
            tokenLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
            registerButton.topAnchor.constraint(equalTo: tokenLabel.bottomAnchor, constant: 40),
            registerButton.centerXAnchor.constraint(equalTo: view.centerXAnchor)
        ])
    }

    @objc private func registerTapped() {
        // TODO: Request notification permission and register for remote notifications
    }

}
Task 1
Task 2
Task 3
Task 4
Task 5
Task 6
Solution
iOS Swift
import UIKit
import UserNotifications
import FirebaseCore
import FirebaseMessaging

@main
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, MessagingDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        FirebaseApp.configure()

        UNUserNotificationCenter.current().delegate = self
        Messaging.messaging().delegate = self

        window = UIWindow(frame: UIScreen.main.bounds)
        window?.rootViewController = PushNotificationViewController()
        window?.makeKeyAndVisible()

        return true
    }

    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        let tokenParts = deviceToken.map { data in String(format: "%02hhx", data) }
        let token = tokenParts.joined()
        print("APNs device token: \(token)")
        NotificationCenter.default.post(name: .didReceiveDeviceToken, object: token)
    }

    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
        print("Failed to register: \(error)")
        NotificationCenter.default.post(name: .didFailToRegister, object: error.localizedDescription)
    }

    // MARK: UNUserNotificationCenterDelegate
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
        return [.banner, .sound]
    }

    // MARK: MessagingDelegate
    func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
        guard let fcmToken = fcmToken else { return }
        print("FCM token: \(fcmToken)")
        NotificationCenter.default.post(name: .didReceiveFCMToken, object: fcmToken)
    }
}

extension Notification.Name {
    static let didReceiveDeviceToken = Notification.Name("didReceiveDeviceToken")
    static let didFailToRegister = Notification.Name("didFailToRegister")
    static let didReceiveFCMToken = Notification.Name("didReceiveFCMToken")
}

class PushNotificationViewController: UIViewController {

    private let statusLabel = UILabel()
    private let tokenLabel = UILabel()
    private let registerButton = UIButton(type: .system)

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        setupUI()
        addObservers()
    }

    private func setupUI() {
        statusLabel.text = "Status: Not Registered"
        statusLabel.translatesAutoresizingMaskIntoConstraints = false
        tokenLabel.numberOfLines = 0
        tokenLabel.translatesAutoresizingMaskIntoConstraints = false
        registerButton.setTitle("Register for Notifications", for: .normal)
        registerButton.translatesAutoresizingMaskIntoConstraints = false
        registerButton.addTarget(self, action: #selector(registerTapped), for: .touchUpInside)

        view.addSubview(statusLabel)
        view.addSubview(tokenLabel)
        view.addSubview(registerButton)

        NSLayoutConstraint.activate([
            statusLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40),
            statusLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            tokenLabel.topAnchor.constraint(equalTo: statusLabel.bottomAnchor, constant: 20),
            tokenLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
            tokenLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
            registerButton.topAnchor.constraint(equalTo: tokenLabel.bottomAnchor, constant: 40),
            registerButton.centerXAnchor.constraint(equalTo: view.centerXAnchor)
        ])
    }

    private func addObservers() {
        NotificationCenter.default.addObserver(self, selector: #selector(didReceiveDeviceToken(_:)), name: .didReceiveDeviceToken, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(didFailToRegister(_:)), name: .didFailToRegister, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(didReceiveFCMToken(_:)), name: .didReceiveFCMToken, object: nil)
    }

    @objc private func registerTapped() {
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in
            DispatchQueue.main.async {
                if let error = error {
                    self.statusLabel.text = "Status: Permission Error"
                    self.tokenLabel.text = error.localizedDescription
                    return
                }
                if granted {
                    self.statusLabel.text = "Status: Permission Granted"
                    UIApplication.shared.registerForRemoteNotifications()
                } else {
                    self.statusLabel.text = "Status: Permission Denied"
                    self.tokenLabel.text = ""
                }
            }
        }
    }

    @objc private func didReceiveDeviceToken(_ notification: Notification) {
        if let token = notification.object as? String {
            DispatchQueue.main.async {
                self.statusLabel.text = "Status: Registered with APNs"
                self.tokenLabel.text = "Device Token:\n\(token)"
            }
        }
    }

    @objc private func didFailToRegister(_ notification: Notification) {
        if let errorMessage = notification.object as? String {
            DispatchQueue.main.async {
                self.statusLabel.text = "Status: Registration Failed"
                self.tokenLabel.text = errorMessage
            }
        }
    }

    @objc private func didReceiveFCMToken(_ notification: Notification) {
        if let fcmToken = notification.object as? String {
            DispatchQueue.main.async {
                self.statusLabel.text = "Status: Registered with FCM"
                self.tokenLabel.text = "FCM Token:\n\(fcmToken)"
            }
        }
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
    }
}

This solution sets up push notifications for an iOS app using both APNs and Firebase Cloud Messaging (FCM). The AppDelegate initializes Firebase and sets itself as the delegate for notification center and messaging. It handles device token registration and errors, posting notifications to update the UI.

The PushNotificationViewController shows the registration status and token. When the user taps the register button, the app requests notification permission. If granted, it registers for remote notifications to get the APNs device token. The app listens for notifications about device token and FCM token updates and displays them on screen.

This approach keeps UI and notification logic separated and uses NotificationCenter to communicate between AppDelegate and the view controller. It also handles permission denial and errors gracefully, updating the status label accordingly.

Final Result
Completed Screen
----------------------------------
| Push Notification Setup         |
----------------------------------
| Status: Registered with FCM     |
|                                |
| FCM Token:                     |
| abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 |
|                                |
| [Register for Notifications]   |
----------------------------------
User taps 'Register for Notifications' button.
App asks for permission to send notifications.
If user allows, app registers with APNs and Firebase.
Status label updates to show registration success.
Device token or FCM token appears below the status.
If permission denied, status updates to 'Permission Denied'.
Stretch Goal
Add support for handling incoming push notifications and display an alert with the notification content when the app is in the foreground.
💡 Hint
Implement the UNUserNotificationCenterDelegate method userNotificationCenter(_:willPresent:withCompletionHandler:) to show alerts while the app is active.