Push notifications with Amazon’s AWS Simple Notifications Service (SNS) for Swift UI and iOS 16+ in 2023
It has been six years since the original tutorial for push notifications with SNS was published — a lot has happened since then. Swift UI has been released and we are now at iOS 17. Now more than ever, is it necessary to use push notifications to inform users and keep them engaged.
We have recently worked on a new app called BadaBoop — an AI-powered service used for the discovery, playback and chatting about music — through which we have learned all we needed to know to create an updated tutorial to enable AWS SNS push notifications for SwiftUI and iOS 16+. If you’re a music lover, please check out BadaBoop by following this link: CLICK HERE.
First Things First
Let’s first create an Apple Push Sandbox Services certificate. This is a development certificate that helps you work with Apple push services when developing your app. Ultimately, when you are done with your development, you would create the Apple Push Services certificate meant for production in exactly the same way. For now, let’s work with the sandbox.
Open XCode and create a new SwiftUI app. Make sure to give it a unique bundle identifier.
Log into your Apple developer account and go to Certificates, Identifiers and Profiles.
Create an app ID. First click on the Identifiers menu item. Then click on Identifiers+ to start creating a new app ID. Make sure that the App ID’s radio button is highlighted and then click the Continue button.
Select the App type and click continue.
Register the app by adding a description and using the same bundle ID you used when creating the app on XCode. Then click Continue.
Confirm that all the information is correct and then click the Register button.
When you’re done creating the app ID, got to Certificates and click on Certificates+.
Scroll down to the Services section and click on the Apple Push Notification service SSL (Sandbox) radio button and then click Continue.
In the Create a new Apple Push Notification service SSL (Sandbox) page, select the app ID you created from the dropdown and click continue.
Open Keychain Access on your computer and request a new certificate by click the Keychain Access > Certificate Assistant > Request a Certificate From a Certificate Authority… menu item.
Enter your user email, choose the Save to disk option and click Continue.
Save the certificate file in a safe location and the click Done when to finish the process.
Go back to your Apple developer account and choose the certificate file you created and click Continue.
Download you newly created Apple Push services certificate.
Double click the newly downloaded file. You should see a new entry in Keychain Access under My Certificates.
Export the new certificate as a p12 by right clicking on it and exporting the certificate.
Save it somewhere safe. You can give it a password or not — it’s up to you. This p12 will be used in AWS when you create a SNS application.
Now, To The Real Work
Download the AWS iOS SDK: download the latest iOS by going to https://github.com/aws-amplify/aws-sdk-ios and downloading the latest SDK or alternatively, click this link HERE.
This SDK, will be used to access AWS’s authentication and push notification services.
Create a new AWS SNS platform application: log into you AWS account and navigate to SNS (Simple Notification Service). Under the Mobile section, click on the Push notifications menu item.
To create a new platform application, which will enable push notifications using SNS, click the Create platform application button.
On the Create platform application page, populate the Application name and set the Push notification platform as Apple iOS/VoIP/MacOS. Since we created a sandbox certificate, check Used for sandbox development. Set the Push service as iOS and the Authentication method as certificate. Choose the p12 file that you created earlier. If you gave it a password, enter the password as well — otherwise, leave it empty. When all of this is done, click the Load credentials from file button. Scroll to the bottom of the page and click the Create platform application button.
A new platform application has been created. It contains an ARN (Amazon Resource Name) that will be used to send push notifications programmatically. So, please note your application’s ARN.
Create an Identity and Role: we will now create a new identity and role that we will use to access AWS resources — in this case, AWS SNS. We will use AWS Cognito and IAM. Cognito is a service that provides application identity and authentication and IAM provisions access in every conceivable way to an AWS account.
It is important to understand that there are numerous ways to achieve what we are going to create in this step — this method just happens to be the easiest for our current task.
Open AWS in a new tab without closing the currently open SNS tab. Go to AWS Cognito. Open Identity Pools and click the Create identity pool button.
In the Configure identity pool trust page, check Guest access and click Next.
In the Configure permissions page, Create a new IAM role and enter your IAM tole name. You can make it something like <app name>-Unauthorized .
Enter the Identity pool name (this can be your app name) and click Next. Then click the Create identity pool button.
Once the identity pool has been create, open it. Please note the Identity pool ID. This will help you retrieved tokens and configure access to resources for your app.
Update IAM role: Open AWS in a new tab without closing the currently open Cognito tab. Go to AWS IAM. Open Roles and find your newly created role and open it. Click the Add permissions dropdown and choose Attach policies.
On the Add permissions page, search for AmazonSNSFullAccess, check it and click Attach permissions.
We are now done with the AWS setup but don’t close those 3 tabs just yet.
The fun begins: Lets start coding!
Go back to you XCode project and navigate to the General tab. Scroll down to the Frameworks, Libraries and Embedded Content section and click the + button. In the dialog’s dropdown, click Add Files… and navigate to the unzipped AWS iOS SDK you downloaded earlier. Add the following files (frameworks): AWSCognitoAuth.xcframework, AWSCognitoIdentityProvider.xcframework, AWSCognitoIdentityProviderASF.xcframework, AWSCore.xcframework and AWSSNS.xcframework. Make sure that you choose Embed & Sign for all of them.
When this process is complete, your project should look like this:
We now have to enable push notification capability for our app. Go to you project and click the Signing & Capabilities tab. Click the + button directly beneath the tab. A capabilities dialog with open. Search for Push Notifications and pick it. Once that’s done, you should see two things: a newly added Push Notifications capability and a new entitlements file name <AppName>.entitlements. Your project should look something like this:
Now that the project setup is complete, let is write some code. Open you app’s entry-point file <AppName>App.swift. Do the following things:
- Beneath the SwiftUI import, import AWSSNS and UserNotifications.
import AWSSNS
import UserNotifications
2. Add a UIApplicationDelegateAdaptor to your main app struct. This is necessary so that you can use an AppDelegate that enables push notifications.
@UIApplicationDelegateAdaptor private var appDelegate: AppDelegate
3. Add an AppDelegate that enables notifications using SNS.
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
/// The SNS Platform application ARN
let SNSPlatformApplicationArn = "Your SNS Application ARN"
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
/// Setup AWS Cognito credentials
let credentialsProvider = AWSCognitoCredentialsProvider(
regionType: AWSRegionType.USEast1, identityPoolId: "Your identity pool ID")
let defaultServiceConfiguration = AWSServiceConfiguration(
region: AWSRegionType.USEast1, credentialsProvider: credentialsProvider)
AWSServiceManager.default().defaultServiceConfiguration = defaultServiceConfiguration
registerForPushNotifications(application: application)
return true
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
/// Attach the device token to the user defaults
var token = ""
for i in 0..<deviceToken.count {
token = token + String(format: "%02.2hhx", arguments: [deviceToken[i]])
}
print(token)
UserDefaults.standard.set(token, forKey: "deviceTokenForSNS")
/// Create a platform endpoint. In this case, the endpoint is a
/// device endpoint ARN
let sns = AWSSNS.default()
let request = AWSSNSCreatePlatformEndpointInput()
request?.token = token
request?.platformApplicationArn = SNSPlatformApplicationArn
sns.createPlatformEndpoint(request!).continueWith(executor: AWSExecutor.mainThread(), block: { (task: AWSTask!) -> AnyObject? in
if task.error != nil {
print("Error: \(String(describing: task.error))")
} else {
let createEndpointResponse = task.result! as AWSSNSCreateEndpointResponse
if let endpointArnForSNS = createEndpointResponse.endpointArn {
print("endpointArn: \(endpointArnForSNS)")
UserDefaults.standard.set(endpointArnForSNS, forKey: "endpointArnForSNS")
}
}
return nil
})
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
print(error.localizedDescription)
}
func registerForPushNotifications(application: UIApplication) {
/// The notifications settings
if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().delegate = self
UNUserNotificationCenter.current().requestAuthorization(options: [.badge, .sound, .alert], completionHandler: {(granted, error) in
if (granted)
{
UIApplication.shared.registerForRemoteNotifications()
}
else{
//Do stuff if unsuccessful...
}
})
} else {
let settings = UIUserNotificationSettings(types: [UIUserNotificationType.alert, UIUserNotificationType.badge, UIUserNotificationType.sound], categories: nil)
application.registerUserNotificationSettings(settings)
application.registerForRemoteNotifications()
}
}
// Called when a notification is delivered to a foreground app.
@available(iOS 10.0, *)
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
print("User Info = ",notification.request.content.userInfo)
completionHandler([.alert, .badge, .sound])
}
// Called to let your app know which action was selected by the user for a given notification.
@available(iOS 10.0, *)
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
print("User Info = ",response.notification.request.content.userInfo)
completionHandler()
}
}
So that in the end, your complete application code should look something like this:
//
// BadaPushTutorialApp.swift
// BadaPushTutorial
//
// Created by Thabo Klass on 05/11/2023.
//
import SwiftUI
import AWSSNS
import UserNotifications
@main
struct BadaPushTutorialApp: App {
@UIApplicationDelegateAdaptor private var appDelegate: AppDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
/// The SNS Platform application ARN
let SNSPlatformApplicationArn = "Your SNS Application ARN"
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
/// Setup AWS Cognito credentials
let credentialsProvider = AWSCognitoCredentialsProvider(
regionType: AWSRegionType.USEast1, identityPoolId: "Your identity pool ID")
let defaultServiceConfiguration = AWSServiceConfiguration(
region: AWSRegionType.USEast1, credentialsProvider: credentialsProvider)
AWSServiceManager.default().defaultServiceConfiguration = defaultServiceConfiguration
registerForPushNotifications(application: application)
return true
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
/// Attach the device token to the user defaults
var token = ""
for i in 0..<deviceToken.count {
token = token + String(format: "%02.2hhx", arguments: [deviceToken[i]])
}
print(token)
UserDefaults.standard.set(token, forKey: "deviceTokenForSNS")
/// Create a platform endpoint. In this case, the endpoint is a
/// device endpoint ARN
let sns = AWSSNS.default()
let request = AWSSNSCreatePlatformEndpointInput()
request?.token = token
request?.platformApplicationArn = SNSPlatformApplicationArn
sns.createPlatformEndpoint(request!).continueWith(executor: AWSExecutor.mainThread(), block: { (task: AWSTask!) -> AnyObject? in
if task.error != nil {
print("Error: \(String(describing: task.error))")
} else {
let createEndpointResponse = task.result! as AWSSNSCreateEndpointResponse
if let endpointArnForSNS = createEndpointResponse.endpointArn {
print("endpointArn: \(endpointArnForSNS)")
UserDefaults.standard.set(endpointArnForSNS, forKey: "endpointArnForSNS")
}
}
return nil
})
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
print(error.localizedDescription)
}
func registerForPushNotifications(application: UIApplication) {
/// The notifications settings
if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().delegate = self
UNUserNotificationCenter.current().requestAuthorization(options: [.badge, .sound, .alert], completionHandler: {(granted, error) in
if (granted)
{
UIApplication.shared.registerForRemoteNotifications()
}
else{
//Do stuff if unsuccessful...
}
})
} else {
let settings = UIUserNotificationSettings(types: [UIUserNotificationType.alert, UIUserNotificationType.badge, UIUserNotificationType.sound], categories: nil)
application.registerUserNotificationSettings(settings)
application.registerForRemoteNotifications()
}
}
// Called when a notification is delivered to a foreground app.
@available(iOS 10.0, *)
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
print("User Info = ",notification.request.content.userInfo)
completionHandler([.alert, .badge, .sound])
}
// Called to let your app know which action was selected by the user for a given notification.
@available(iOS 10.0, *)
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
print("User Info = ",response.notification.request.content.userInfo)
completionHandler()
}
}
Three very important things are happening in this code:
- We have a variable call SNSPlatformApplicationArn. The value of this variable is a string and is equal to the ARN of the SNS application you created earlier. Copy that string into your code.
- Inside func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool, we have to enter the identity pool ID. You can get this from the new identity pool you created in AWS Cognito. Copy the identity pool ID and paste it into your code.
- We are also saving the retrieved deviceTokenForSNS and endpointArnForSNS in the UserDefaults. These two are important should you ever want to send a unique device a notification programmatically.
Once all of the above is done, connect your iOS device to XCode and run your application on the device. A new endpoint will now appear in your SNS platform application.
And now finally, you can send your new endpoint and push notification. First, click on the endpoint to open it. Click the Publish message button. Scroll down to Message body and type your message. Once done, click the Publish message button.
That’s it! You should receive a notification on the device that has your app. Congratulations, you now have a working example of SNS push notifications for SwiftUI on iOS 16+.
Sending a message to a unique device programmatically: for this, we need the device token (deviceTokenForSNS) and the endpoint ARN (endpointArnForSNS). If we have these two bits of information, we can use the code below:
if let deviceArn = userDict["endpointArnForSNS"] as? String, let deviceTokenSNS = userDict["deviceTokenForSNS"] as? String {
/// Push notification meant for the spreebie uploader
let sns = AWSSNS.default()
let request = AWSSNSPublishInput()
request?.messageStructure = "json"
/// The payload
let dict = ["default": "New direct message from BadaBoop.", "APNS_SANDBOX": "{\"aps\":{\"alert\": {\"title\":\"New message from BadaBoop \",\"body\":\"The BadaBoop music playback is great.\"},\"sound\":\"default\",\"badge\":1} }"]
do {
let jsonData = try JSONSerialization.data(withJSONObject: dict, options: JSONSerialization.WritingOptions.prettyPrinted)
request?.message = NSString(data: jsonData, encoding: String.Encoding.utf8.rawValue) as? String
request?.targetArn = deviceArn
sns.publish(request!).continueWith
{
(task) -> AnyObject? in
if task.error != nil
{
print("Error sending mesage: \(String(describing: task.error))")
}
else
{
print("Success sending message")
}
return nil
}
} catch {
print(error)
}
}
Hope you enjoyed the tutorial. If you found this tutorial useful, please check out our app BadaBoop which uses the same lessons in this tutorial. Happy coding!