Getting started: Push Notifications in Swift
This guide will get you started with Ably Push Notifications in a new SwiftUI application.
You'll learn how to set up your AppDelegate to manage push notifications, register devices with Ably, publish push notifications, subscribe to channel-based push, handle incoming notifications, and implement location-based push notifications.
Prerequisites
- Sign up for an Ably account.
- Create a new app, and create your first API key in the API Keys tab of the dashboard.
- Your API key needs the
publishandsubscribecapabilities. - Also add the
push-admincapability if you're using the same API key to publish a push notification. In production this would more likely be a server using a different API key.
- Add a rule to a channel so you can test sending push notifications via a channel. Select Rules in the Ably dashboard, add a new rule and enable the Push notifications option.
- Install Xcode.
- You'll need a real iOS device to test push notifications (the simulator doesn't support APNs).
- Set up Apple Push Notification service (APNs) certificates through the Apple Developer Portal.
(Optional) Install Ably CLI
Use the Ably CLI as an additional client to quickly test Pub/Sub features and push notifications.
ably init installs the Ably CLI, authenticates, and sets the default app and API key in a single command:
npx -p @ably/cli ably initSet up APNs certificates
To enable push notifications, you need to configure APNs on Apple's developer portal:
- Go to Apple Developer Portal.
- Create an App ID for your application (if you don't have one already).
- Enable the Push Notifications capability for your App ID.
- Create an APNs certificate and download it.
- In the Ably dashboard, navigate to your app's Notifications tab.
- Scroll to the Push Notifications Setup section and select Configure Push.
- Follow the instructions to upload your APNs certificate.
Create a Swift project with Xcode
Create a new iOS SwiftUI project and add the Ably SDK dependency to it:
- In Xcode, go to File > Add Package Dependencies
- Enter the repository URL: https://github.com/ably/ably-cocoa
- Select the latest version and add it to your target
Update your project settings:
- Select the target for your app and go to the Signing & Capabilities tab.
- Make sure you've selected your development team and that a provisioning profile has been created.
- Add the Push Notifications capability by clicking + Capability.
All further code can be added directly to your ContentView.swift and AppDelegate.swift files.
Step 1: Set up Ably
Create an AppDelegate.swift file and add the AppDelegate class which should conform to the following protocols:
UIApplicationDelegate, ARTPushRegistererDelegate, UNUserNotificationCenterDelegate, and CLLocationManagerDelegate.
Set up the Ably realtime client, notification center, and location manager in your
application:didFinishLaunchingWithOptions delegate method as shown below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import Ably
import UIKit
import CoreLocation
import UserNotifications
class AppDelegate: NSObject, UIApplicationDelegate, ARTPushRegistererDelegate, UNUserNotificationCenterDelegate, CLLocationManagerDelegate {
// MARK: - Properties
var realtime: ARTRealtime!
var locationManager: CLLocationManager!
var defaultDeviceToken: String?
var locationDeviceToken: String?
var activatePushCallback: ((String, ARTErrorInfo?) -> ())?
var activateLocationPushCallback: ((String, ARTErrorInfo?) -> ())?
var locationGrantedCallback: ((Bool) -> ())?
// MARK: - UIApplicationDelegate Methods
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
// Initialize Ably Realtime client with push registerer delegate
let clientOptions = ARTClientOptions(key: "demokey:*****")
clientOptions.clientId = "push-tutorial-client"
clientOptions.pushRegistererDelegate = self
realtime = ARTRealtime(options: clientOptions)
// Set up notification delegate
UNUserNotificationCenter.current().delegate = self
// Setup location manager for location-based push
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
// Subscribe to realtime messages on the channel
subscribeToRealtime("my-first-push-channel")
return true
}
}Here you also have some properties defined to manage device tokens and callbacks for the UI which we'll use later.
Step 2: Set up push notifications
To send and receive push notifications, you need to provide ably-cocoa with the device token received
from Apple in the application:didRegisterForRemoteNotificationsWithDeviceToken delegate method.
You also need to request notification permissions from the user and register your device with Ably.
To handle registration results, you'll implement the ARTPushRegistererDelegate methods.
Getting device details is also useful to confirm that your device is registered correctly.
Append the following code to your AppDelegate class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
defaultDeviceToken = deviceToken.map { String(format: "%02x", UInt($0)) }.joined() // Convert device token data to a hex string
print("Device Token registered: \(defaultDeviceToken!)")
// Use Ably's global ARTPush method to register the device token with Ably
ARTPush.didRegisterForRemoteNotifications(withDeviceToken: deviceToken, realtime: realtime)
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
print("Failed to register for remote notifications: \(error.localizedDescription)")
// Use Ably's global ARTPush method to handle registration failure
ARTPush.didFailToRegisterForRemoteNotificationsWithError(error, realtime: realtime)
}
// MARK: - Push Notifications Methods
/// Request notification permissions from user
func requestUserNotificationAuthorization() {
// Request authorization for alerts, sounds, and badges
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
DispatchQueue.main.async {
if granted {
print("Notification permissions granted")
} else if let error = error {
print("Notification permission error: \(error.localizedDescription)")
}
}
}
}
/// Activate push notifications
func activatePushNotifications(_ callback: @escaping (String, ARTErrorInfo?) -> ()) {
// Store callback since activation is asynchronous
activatePushCallback = callback
// Request notification permissions
requestUserNotificationAuthorization()
// Activate push notifications with Ably
realtime.push.activate()
print("Activating push notifications...")
}
/// Deactivate push notifications
func deactivatePush() {
realtime.push.deactivate()
print("Deactivating push notifications...")
}
// MARK: - ARTPushRegistererDelegate Methods
func didActivateAblyPush(_ error: ARTErrorInfo?) {
print("Push activation: \(error?.localizedDescription ?? "Success")")
// Notify UI about activation result
activatePushCallback?(realtime.device.id, error)
}
func didDeactivateAblyPush(_ error: ARTErrorInfo?) {
print("Push deactivation: \(error?.localizedDescription ?? "Success")")
}Now you are ready to receive push notifications.
Handle push notifications
Use UNUserNotificationCenterDelegate methods to receive push notifications.
You've set the notification center delegate in the application:didFinishLaunchingWithOptions method.
Add these methods to your AppDelegate class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// MARK: - UNUserNotificationCenterDelegate Methods
/// Handle notification when app is in foreground
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
let userInfo = notification.request.content.userInfo
print("Notification received in foreground: \(userInfo)")
// Display notification with banner, sound, and badge
completionHandler([.banner, .sound, .badge])
}
/// Handle notification when user taps on the notification when the app is in the background
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
let userInfo = response.notification.request.content.userInfo
print("Notification tapped: \(userInfo)")
completionHandler()
}Step 3: Subscribe to channel push notifications
To subscribe your device to a channel so it can receive channel-based push notifications, add the following methods to your AppDelegate class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// MARK: - Subscribe to Channels
/// Subscribe to realtime messages on a channel
func subscribeToRealtime(_ channelName: String) {
let channel = realtime.channels.get(channelName)
channel.subscribe { message in
print("Received message: \(message.name ?? "") - \(String(describing: message.data))")
}
}
/// Subscribe to a channel for push notifications
func subscribeToChannel(_ channelName: String) {
let channel = realtime.channels.get(channelName)
channel.push.subscribeDevice { error in
if let error = error {
print("Error subscribing to channel push: \(error.localizedDescription)")
} else {
print("Subscribed to push notifications on channel: \(channelName)")
}
}
}
/// Unsubscribe from a channel
func unsubscribeFromChannel(_ channelName: String) {
let channel = realtime.channels.get(channelName)
channel.push.unsubscribeDevice { error in
if let error = error {
print("Error unsubscribing from channel push: \(error.localizedDescription)")
} else {
print("Unsubscribed from push notifications on channel: \(channelName)")
}
}
}Step 4: Build the UI
First, in your PushTutorialApp.swift, add @UIApplicationDelegateAdaptor wrapped appDelegate property
to your app @main struct and pass it to the ContentView:
1
2
3
4
5
6
7
8
9
10
11
12
import SwiftUI
@main
struct PushTutorialApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView(appDelegate: appDelegate)
}
}
}Then, update your ContentView.swift to accept the appDelegate and display a few sections:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import Ably
import SwiftUI
struct ContentView: View {
let appDelegate: AppDelegate
@State private var statusMessage = "Ready to start"
var body: some View {
NavigationStack {
VStack(spacing: 0) {
// Status Section (always visible at the top)
StatusSection(message: $statusMessage)
.padding()
// Scrollable sections
ScrollView {
VStack(spacing: 16) {
// Setup Section
SetupPushSection(appDelegate: appDelegate, statusMessage: $statusMessage)
// Subscribe to Channel Section
ChannelSection(appDelegate: appDelegate, statusMessage: $statusMessage)
}
.padding()
}
}
.navigationTitle("Push Tutorial")
}
}
}Each section is implemented as a separate SwiftUI View struct for better organization.
Since this is not a SwiftUI tutorial, we will not go into details of each section's implementation.
They are just a few buttons with some basic styling. You can add this code at the bottom of the same ContentView.swift file:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
// MARK: - Status Section
struct StatusSection: View {
@Binding var message: String
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Status")
.font(.headline)
Text(message)
.font(.caption)
.foregroundStyle(.secondary)
.padding(12)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
}
.padding()
.background(Color.white)
.cornerRadius(12)
.shadow(radius: 2)
}
}
// MARK: - Setup Section
struct SetupPushSection: View {
let appDelegate: AppDelegate
@Binding var statusMessage: String
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Setup")
.font(.headline)
VStack(spacing: 10) {
Button(action: {
appDelegate.activatePushNotifications { deviceId, error in
if let error = error {
statusMessage = "Push activation failed: \(error.localizedDescription)"
} else {
statusMessage = "Push activated. Device ID: \(deviceId)"
}
}
statusMessage = "Activating push notifications..."
}) {
HStack {
Image(systemName: "checkmark.circle")
Text("Activate Push")
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.green)
.foregroundStyle(.white)
.cornerRadius(8)
}
Button(action: {
appDelegate.deactivatePush()
statusMessage = "Push notifications deactivated"
}) {
HStack {
Image(systemName: "xmark.circle")
Text("Deactivate Push")
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.red)
.foregroundStyle(.white)
.cornerRadius(8)
}
}
}
.padding()
.background(Color.white)
.cornerRadius(12)
.shadow(radius: 2)
}
}
// MARK: - Channel Section
struct ChannelSection: View {
let appDelegate: AppDelegate
@Binding var statusMessage: String
private let channelName = "my-first-push-channel"
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Channel Subscription")
.font(.headline)
VStack(spacing: 10) {
Button(action: {
appDelegate.subscribeToChannel(channelName)
statusMessage = "Subscribed to: \(channelName)"
}) {
HStack {
Image(systemName: "checkmark.circle.fill")
Text("Subscribe to Channel")
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.purple)
.foregroundStyle(.white)
.cornerRadius(8)
}
Button(action: {
appDelegate.unsubscribeFromChannel(channelName)
statusMessage = "Unsubscribed from: \(channelName)"
}) {
HStack {
Image(systemName: "xmark.circle.fill")
Text("Unsubscribe from Channel")
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.orange)
.foregroundStyle(.white)
.cornerRadius(8)
}
}
}
.padding()
.background(Color.white)
.cornerRadius(12)
.shadow(radius: 2)
}
}Build and run your app in Xcode on a real device. You will see the UI with sections to activate push notifications and subscribe to channels.
Step 5: Publish a push notification
Tap Activate Push and wait until the status message displays your device ID.
Publish directly to your device
Publish a push notification directly to your client ID (or device ID using --device-id instead of --client-id) via the Ably CLI:
ably push publish --client-id push-tutorial-client \
--title "Test push" \
--body "Hello from CLI!" \
--data '{"foo":"bar","baz":"qux"}'Publish via a channel
Tap Subscribe to Channel in the UI, then publish a push notification to the channel using the Ably CLI:
ably push publish --channel my-first-push-channel \
--title "Hello" \
--body "World!" \
--message '{"name":"greeting","data":"Hello World!"}'
If you unsubscribe from this channel in the app's UI, you will no longer receive push notifications for that channel. Run the same command again and verify that no notification is received.
To see the full list of options for publishing push notifications with the Ably CLI, run ably push publish --help or see the Ably CLI push documentation. To publish push notifications from your own server code instead of the CLI, see Push notification publishing.
Step 6: Location pushes
Starting from iOS 15, you can efficiently receive location requests as push notifications. To do this, you need to apply for the special entitlement on the Apple Developer Portal.
Add Location (when in use), Location (Always), Location Push Service Extension, and
Push Notifications capabilities to the Signing & Capabilities tab in the Xcode project target settings.
Add Location Push Service Extension target as described at the Apple Developer Portal.
For simplicity, use Automatically manage signing, so all needed identifiers are created for you by
Xcode (with XC prefix in their display name). Your Location Push Service Extension should have a bundle
identifier of your app with a suffix of extension's product name (for example, the.company.TheApp.TheExtension).
Add these methods to your AppDelegate class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// MARK: - Location push methods
/// Enable location push monitoring
func enableLocationPush(grantedCallback: @escaping (Bool) -> (), tokenCallback: @escaping (String, ARTErrorInfo?) -> ()) {
// Store callbacks since location permission request is asynchronous
locationGrantedCallback = grantedCallback
activateLocationPushCallback = tokenCallback
switch locationManager.authorizationStatus {
case .authorizedAlways:
// Location permissions already granted
locationGrantedCallback?(true)
// Activate location push monitoring
activateLocationPush()
print("Location push enabled")
case .notDetermined:
// Request location permissions from the user with 'Always' authorization needed for location pushes
locationManager.requestAlwaysAuthorization()
case .denied, .restricted, .authorizedWhenInUse:
locationGrantedCallback?(false)
print("Location permission denied or restricted")
@unknown default:
break
}
}
/// Disable location push monitoring
func disableLocationPush() {
locationManager?.stopUpdatingLocation()
print("Location push disabled")
}
/// Activate location push monitoring
func activateLocationPush() {
print("Starting monitoring location pushes...")
locationManager.startMonitoringLocationPushes { deviceToken, error in
guard error == nil else {
return ARTPush.didFailToRegisterForLocationNotificationsWithError(error!, realtime: self.realtime)
}
if let deviceToken {
// Convert device token data to a hex string
self.locationDeviceToken = deviceToken.map { String(format: "%02x", UInt($0)) }.joined()
// Provide Ably with location device token
ARTPush.didRegisterForLocationNotifications(withDeviceToken: deviceToken, realtime: self.realtime)
print("Location push activated with device token: \(self.locationDeviceToken!)")
}
}
}
// MARK: - CLLocationManagerDelegate Methods
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
switch manager.authorizationStatus {
case .authorizedAlways:
// Location permissions granted, activate location push monitoring
locationGrantedCallback?(true)
// Activate location push monitoring
activateLocationPush()
print("Location services always authorized.")
case .notDetermined, .authorizedWhenInUse, .restricted, .denied:
// Inform UI that location permissions were not granted
locationGrantedCallback?(false)
print("Location services unavailable for location pushes.")
break
default:
break
}
}Also add this in your ARTPushRegistererDelegate section.
It will be called after ARTPush.didRegisterForLocationNotifications(withDeviceToken:realtime:) completes:
1
2
3
4
5
6
7
func didUpdateAblyPush(_ error: ARTErrorInfo?) {
print("Push update: \(error?.localizedDescription ?? "Success")")
if let locationDeviceToken {
// Notify UI about activation result
activateLocationPushCallback?(locationDeviceToken, error)
}
}Receive location pushes
Once you've added the location push extension to your project, Xcode gives you a default implementation of the
LocationPushService.swift file in your extension target.
Use locationManager(_:didUpdateLocations) delegate method to handle location updates as needed.
Now add the LocationPushSection to your ContentView.swift file to enable location push from the UI:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// MARK: - Location Push Section
struct LocationPushSection: View {
let appDelegate: AppDelegate
@Binding var statusMessage: String
@State var isLocationPushEnabled = false
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Location Push")
.font(.headline)
VStack(spacing: 10) {
Button(action: {
appDelegate.enableLocationPush { granted in
if granted {
isLocationPushEnabled = true
statusMessage = "Location push authorization granted."
} else {
isLocationPushEnabled = false
statusMessage = "Location push authorization denied or restricted."
}
} tokenCallback: { deviceToken, error in
if let error = error {
isLocationPushEnabled = false
statusMessage = "Location push activation failed: \(error.localizedDescription)"
} else {
statusMessage = "Location push notifications activated with device token: \(deviceToken)"
}
}
}) {
HStack {
Image(systemName: "mappin.circle.fill")
Text("Enable Location Push")
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.green)
.foregroundStyle(.white)
.cornerRadius(8)
}
Button(action: {
appDelegate.disableLocationPush()
isLocationPushEnabled = false
statusMessage = "Location push disabled"
}) {
HStack {
Image(systemName: "mappin.circle")
Text("Disable Location Push")
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.gray)
.foregroundStyle(.white)
.cornerRadius(8)
}
HStack {
Image(systemName: isLocationPushEnabled ? "checkmark.circle.fill" : "xmark.circle")
Text(isLocationPushEnabled ? "Location Push: Enabled" : "Location Push: Disabled")
Spacer()
}
.font(.caption)
.foregroundStyle(isLocationPushEnabled ? .green : .gray)
.padding(8)
.background(Color.gray.opacity(0.1))
.cornerRadius(6)
}
}
.padding()
.background(Color.white)
.cornerRadius(12)
.shadow(radius: 2)
}
}Don't forget to include this section in your ContentView body (add it just after the SendPushSection):
1
2
// Location Push Section
LocationPushSection(appDelegate: appDelegate, statusMessage: $statusMessage)Build and run your app. Enable location push from the UI and grant location permissions when prompted. Use Ably dashboard, Apple dashboard, or Ably CLI to send location push notifications to your device.
You can also use the following cURL command to send location pushes:
curl -v \
--header "authorization: bearer ${AUTHENTICATION_TOKEN}" \
--header "apns-topic: ${BUNDLE_ID}.location-query" \
--header "apns-push-type: location" \
--data '{"aps":{}}' \
--http2 https://api.development.push.apple.com:443/3/device/${DEVICE_TOKEN}Replace ${BUNDLE_ID} with your app's bundle identifier, ${AUTHENTICATION_TOKEN} with your APNs authentication token,
and ${DEVICE_TOKEN} with the location device token you received in the console logs after enabling location push
(don't confuse it with the device ID).
Read how to obtain the AUTHENTICATION_TOKEN on Apple Developer Portal.
Verify that your app receives and handles location push notifications correctly in the LocationPushService class.
Next steps
- Check the push example
- Understand token authentication before going to production.
- Explore push notification administration for managing devices and subscriptions.
- Learn about rules for channel-based push notifications.
- Read more about the Push Admin API.
- Check out the Push Notifications documentation for advanced use cases.
- Explore Ably CLI push commands for the full list of push CLI options.
You can also explore the Ably SDK for Swift on GitHub, or visit the API references for additional functionality.