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

  1. Sign up for an Ably account.
  2. Create a new app, and create your first API key in the API Keys tab of the dashboard.
  • Your API key needs the publish and subscribe capabilities.
  • Also add the push-admin capability 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.
  1. 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.
  2. Install Xcode.
  3. You'll need a real iOS device to test push notifications (the simulator doesn't support APNs).
  4. 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 init

Set up APNs certificates

To enable push notifications, you need to configure APNs on Apple's developer portal:

  1. Go to Apple Developer Portal.
  2. Create an App ID for your application (if you don't have one already).
  3. Enable the Push Notifications capability for your App ID.
  4. Create an APNs certificate and download it.
  5. In the Ably dashboard, navigate to your app's Notifications tab.
  6. Scroll to the Push Notifications Setup section and select Configure Push.
  7. 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:

Update your project settings:

  1. Select the target for your app and go to the Signing & Capabilities tab.
  2. Make sure you've selected your development team and that a provisioning profile has been created.
  3. 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:

Swift

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
    }
}
API key:
DEMO ONLY

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:

Swift

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:

Swift

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:

Swift

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:

Swift

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:

Swift

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:

Swift

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!"}'
Screenshot of the Swift push tutorial application

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:

Swift

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:

Swift

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:

Swift

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):

Swift

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.

Screenshot of the Swift push tutorial application with location pushes

Next steps

You can also explore the Ably SDK for Swift on GitHub, or visit the API references for additional functionality.