iOS Push Notifications: Part 1 — Intro
First time iPhone users heard about “push notifications” was in 2009. Back then this services allowed third party apps receive an incoming data. Considering poor battery and “single active app mode” it was the only solution.
As I write this article it is year 2024. Users have iPhone 15 with iOS 17.3.1. We barely can imagine app without push notifications. Let’s review what is available now, how can we configure our app and backend to send, receive and handle “pushes”.
Setup on portal
In order to send notifications you need configure connection to APNs (Apple Push Notifications service). There are two ways to achieve this:
- using certificates
- using keys
Certificates should be created per app and separately for development and production environments. Those also expire once a year and you need to monitor and re-new them. Keys have no expiration dates. But you can only create two in Apple Developer Account. If you are a large company with many different products, I recommend use certificates. You don’t want share keys between teams and re–generate them once those leaked. In a small company or solo developer keys are the way to go. In a developer portal open Certificates, Identifiers & Profiles section → Keys. Choose to create new, provide name and select Apple Push Notifications service (APNs) enabled. You must save Key ID identifier and download *.p8 file as this won’t be available for download any later.
When you create app identifier on make sure Push Notification capability selected.
Here you should also provide certificates for APNs (if you choose to use them). Configure button appear only after you create app identifier, and start editing it. Since for this tutorial I use key, nothing to do here.
Setup in Xcode
After you create project in Xcode, set proper Bundle ID, navigate to projects’s Signing & Capabilities tab and add new capability: Push Notifications.
The rest should be done in code. You should be familiar with “app delegate” concept. The thing is, even if you use a SwiftUI app, app delegate is the only way to configure push notification handling.
So, if you don’t have an app delegate, let’s create simple AppDelegate.swift with following code.
import UIKit
class AppDelegate: NSObject, UIApplicationDelegate {
public let rootViewModel: RootViewModel = RootViewModel()
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
print("\(#function)\n\(launchOptions ?? [:])")
// 1
UNUserNotificationCenter.current().delegate = self
// 2
application.registerForRemoteNotifications()
return true
}
func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
// 3
print("\(#function)\n[\(deviceToken.map { String(format: "%02.2hhx", $0) }.joined())]")
}
func application(
_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error
) {
// 4
print("\(#function)\n\(error)")
}
}
// MARK: - UNUserNotificationCenterDelegate
extension AppDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification
) async -> UNNotificationPresentationOptions {
// 5
let userInfo = notification.request.content.userInfo
print("\(#function)\n\(userInfo)")
return [.sound, .banner, .badge, .list]
}
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse
) async {
// 6
let userInfo = response.notification.request.content.userInfo
print("\(#function)\n\(userInfo)")
}
}
Here is the short explanation.
- Assign self as delegate
UNUserNotificationCenterDelegate
. - Super important! Trigger to register for notifications process. This should be done on every app launch even before user is asked for permission to receive notifications.
- If registration succeed, the device token data received. Usually this should be forwarded to you server which is going to send push notifications. A device token is an device address from APNs perspective. Sometimes it can change, although it is not clear when. So, be ready to updated it on backend once new token data received.
- If registration fails, you receive an error, hopefully with explanation. I personally, never saw this happening.
- When your app is in foreground and push notification received, this callback is called. You may add custom logic, handle push and return empty array, so no visual representations appear to user. Do this if you want navigate user to some specific screen and avoid showing push banner.
- If your app is in background or not running, after tap on push banner this callback called.
Now let’s inject our AppDelegate into our SwiftUI app
@main
struct Push_Notifications_DemoApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
var body: some Scene {
WindowGroup {
RootView(viewModel: delegate.rootViewModel)
}
}
}
Get user permission
Just before you start sending million of useless pushes to your customers, you have to ask permission.
To check current status, use shared instance of UNUserNotificationCenter
from UserNotifications
framework.
let status = await UNUserNotificationCenter.current()
.notificationSettings().authorizationStatus
The status can be one of these:
notDetermined
— is a default value. User was never prompted about notifications.denied
— user explicitly denied push notifications. This can be changed from inside app setting in Settings app.authorized
— user explicitly allowed pushes. This can be changed from inside app setting in Settings app.ephemeral
— used for App Clips only. Meaning app is authorised for 8 hours to receive a push info.provisional
— this is so called “silent push”. The idea for this is to send push without asking permission. We’ll get to back to this a bit later.
So, if status is notDetermined it is right time to ask user. Note, you can do this only once. No matter what user replies, it can also be changed later from inside settings, but dialog won’t be shown second time. That is crucial to inform user why are you trying to bother him. The text on screenshot above “Allow me to spam?” is configurable and localised. In the info.plist file or Info tab in Xcode target add Privacy — User Notifications Usage Description string. To localise, use InfoPlist.string file with a key NSUserNotificationsUsageDescription. If you do not provide your own explanation, a default message will be used.
do {
try await UNUserNotificationCenter.current()
.requestAuthorization(options: [.alert, .badge, .sound])
} catch {
print(error)
}
As you may guess from source code above we do ask permission which might include alert banner, app badge and sound. User may additionally configure custom setup in app settings, i.e. allow banner but never change badge.
The problem with this dialog is that user may not know your app good enough and don’t want to be disturbed by yet another annoying toy. That’s where provisional
setup comes to help. If you request permission but instead of alert, badge and sound add only [.provisional]
, a dialog won’t be shown, but app will be allowed to receive notifications. Those however only displayed in Notification Center with custom buttons. The idea is to present user with sample notifications what might come in future and ask to either explicitly allow or deny those.
Keep in mind, you can’t ask to provisional
after normal. But you can ask for normal permissions after requesting provisional.
Feel free to play with a project.