Getting started: Push Notifications in Swift

Open in

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, send 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.
  3. Your API key will need the publish and subscribe capabilities. For sending push notifications from your app, you'll also need the push-admin capability.
  4. Install Xcode.
  5. You'll need a real iOS device to test push notifications (the simulator doesn't support APNs).
  6. 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.

  1. Install the Ably CLI:
npm install -g @ably/cli
  1. Run the following to log in to your Ably account and set the default app and API key:
ably login

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

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

        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

58

59

60

61

62

63

64

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...")
}

/// Get current device registration details
func getDeviceDetails(_ callback: @escaping (ARTDeviceDetails?, ARTErrorInfo?) -> ()) {
    realtime.push.admin.deviceRegistrations.get(realtime.device.id, callback: callback)
}

// MARK: - ARTPushRegistererDelegate Methods

func didActivateAblyPush(_ error: ARTErrorInfo?) {
    print("Push activation: \(error?.localizedDescription ?? "Success")")
    if let defaultDeviceToken {
        // Notify UI about activation result
        activatePushCallback?(defaultDeviceToken, error)
    }
}

func didDeactivateAblyPush(_ error: ARTErrorInfo?) {
    print("Push deactivation: \(error?.localizedDescription ?? "Success")")
}

Step 3: Receive 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()
}

Push notifications can be sent either directly to your device ID (or client ID), or posted to a channel, in which case you first need to subscribe your device to that channel:

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

// MARK: - Subscribe to Channels

/// 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)")
        }
    }
}

Sending push notifications using device ID or client ID requires the push-admin capability for your API key. Use this method for testing purposes. In a production environment, you would typically send push notifications from your backend server (by posting messages with push extras field to a channel).

To test push notifications in your app, you can use Ably dashboard, Apple developer dashboard or Ably CLI.

To send to your client ID using Ably CLI paste the following command into your terminal:

ably push publish --client-id push-tutorial-client \
  --title "Test push" \
  --body "Hello from CLI!" \
  --data '{"foo":"bar","baz":"qux"}'

For sending pushes via a channel, we need some actual UI to be able to subscribe to this channel. So, let's build one.

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

32

import Ably
import SwiftUI

struct ContentView: View {
    let appDelegate: AppDelegate

    @State private var statusMessage = "Ready to start"
    @State private var selectedChannel = "exampleChannel1"

    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, selectedChannel: $selectedChannel)
                    }
                    .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

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

// 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 { deviceToken, error in
                        if let error = error {
                            statusMessage = "Push activation failed: \(error.localizedDescription)"
                        } else {
                            statusMessage = "Push notifications activated with device token: \(deviceToken)"
                        }
                    }
                    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)
                }

                Button(action: {
                    appDelegate.getDeviceDetails { details, error in
                        if let details = details {
                            print("Device details: \(details)")
                            statusMessage = """
                            Device ID: \(details.id)
                            Client ID: \(details.clientId ?? "n/a")
                            Platform: \(details.platform)
                            Form Factor: \(details.formFactor)
                            """
                        } else {
                            statusMessage = "Failed to retrieve device details: \(error?.localizedDescription ?? "Unknown error")"
                        }
                    }
                }) {
                    HStack {
                        Image(systemName: "info.circle.fill")
                        Text("Get Device Details")
                    }
                    .frame(maxWidth: .infinity)
                    .padding()
                    .background(Color.blue)
                    .foregroundStyle(.white)
                    .cornerRadius(8)
                }
            }
        }
        .padding()
        .background(Color.white)
        .cornerRadius(12)
        .shadow(radius: 2)
    }
}

// MARK: - Channel Section

// Helper to get a user-friendly title for a channel
func titleForChannel(_ name: String) -> String {
    let titles = [
        "exampleChannel1": "Channel 1",
        "exampleChannel2": "Channel 2"
    ]
    return titles[name] ?? name
}

struct ChannelSection: View {
    let appDelegate: AppDelegate
    @Binding var statusMessage: String
    @Binding var selectedChannel: String

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("Channel Subscription")
                .font(.headline)

            VStack(spacing: 10) {
                HStack(spacing: 8) {
                    Menu {
                        Button(titleForChannel("exampleChannel1")) {
                            selectedChannel = "exampleChannel1"
                        }
                        Button(titleForChannel("exampleChannel2")) {
                            selectedChannel = "exampleChannel2"
                        }
                    } label: {
                        HStack {
                            Image(systemName: "line.3.horizontal.decrease.circle")
                            Text(titleForChannel(selectedChannel))
                        }
                        .frame(maxWidth: .infinity)
                        .padding()
                        .background(Color.gray.opacity(0.2))
                        .cornerRadius(8)
                    }

                    Button(action: {
                        appDelegate.subscribeToChannel(selectedChannel)
                        statusMessage = "Subscribed to: \(titleForChannel(selectedChannel))"
                    }) {
                        HStack {
                            Image(systemName: "checkmark.circle.fill")
                            Text("Subscribe")
                        }
                        .frame(maxWidth: .infinity)
                        .padding()
                        .background(Color.indigo)
                        .foregroundStyle(.white)
                        .cornerRadius(8)
                    }
                }

                Button(action: {
                    appDelegate.unsubscribeFromChannel(selectedChannel)
                    statusMessage = "Unsubscribed from: \(titleForChannel(selectedChannel))"
                }) {
                    HStack {
                        Image(systemName: "xmark.circle.fill")
                        Text("Unsubscribe from \(titleForChannel(selectedChannel))")
                    }
                    .frame(maxWidth: .infinity)
                    .padding()
                    .background(Color.gray)
                    .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. Tap the "Activate Push" button and wait until the status message displays the received device token. Try sending push using client ID or device ID as shown earlier. You can get your device ID from the device details button (don't confuse it with the device token):

Screenshot of the Swift push tutorial application

Send push via channel

To test pushes via channel, subscribe to "Channel 1" in the UI and post a message to the "exampleChannel1" with a push extras field using Ably CLI:

ably channels publish --api-key "demokey:*****" exampleChannel1 '{"data":{"foo":"bar","baz":"qux"},"extras":{"push":{"notification":{"title":"Test push","body":"Hello from CLI!"}}}}'

If you unsubscribe from this channel in the app's UI, you will no longer receive push notifications for that channel. Send the same command again and verify that no notification is received.

You can also send push notifications right from your app. The next step will show you how.

Step 5: Send pushes with code

Just as you can send push notifications through the Ably CLI or dashboard, you can also send them directly from your app using device ID (or client ID), or channel publishing methods. For channel publishing, you don't need the admin capabilities for your API key.

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

36

37

38

39

40

41

// MARK: - Send Push Notifications

/// Send push notification to a specific device ID
func sendPushToDevice() {
    let recipient = [
        "deviceId": realtime.device.id
    ]
    let data = [
        "notification": [
            "title": "Push Tutorial",
            "body": "Hello from device ID!"
        ],
        "data": [
            "foo": "bar",
            "baz": "qux"
        ]
    ]
    realtime.push.admin.publish(recipient, data: data) { error in
        print("Publish result: \(error?.localizedDescription ?? "Success")")
    }
}

/// Send push notification to a specific client ID
func sendPushToClientId() {
    let recipient = [
        "clientId": realtime.auth.clientId ?? "push-tutorial-client"
    ]
    let data = [
        "notification": [
            "title": "Push Tutorial",
            "body": "Hello from client ID!"
        ],
        "data": [
            "foo": "bar",
            "baz": "qux"
        ]
    ]
    realtime.push.admin.publish(recipient, data: data) { error in
        print("Publish result: \(error?.localizedDescription ?? "Success")")
    }
}

Sending to a channel is just publishing a message on a channel with a push extras field:

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

/// Send push notification to a specific channel by publishing a message with a push extras field
func sendPushToChannel(_ channelName: String) {
    let message = ARTMessage(name: "example", data: "Hello from channel!")
    message.extras = [
        "push": [
            "notification": [
                "title": "Channel Push",
                "body": "Sent push to \(channelName)"
            ],
            "data": [
                "foo": "bar",
                "baz": "qux"
            ]
        ]
    ] as any ARTJsonCompatible

    realtime.channels.get(channelName).publish([message]) { error in
        if let error {
            print("Error sending push to \(channelName) with error: \(error.localizedDescription)")
        } else {
            print("Sent push to \(channelName)")
        }
    }
}

Now add buttons for these methods in the new SendPushSection view struct:

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

// MARK: - Send Push Section

struct SendPushSection: View {
    let appDelegate: AppDelegate
    @Binding var statusMessage: String
    @Binding var selectedChannel: String

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("Send Push Notifications")
                .font(.headline)

            VStack(spacing: 10) {
                Button(action: {
                    appDelegate.sendPushToDevice()
                    statusMessage = "Sending push to device ID..."
                }) {
                    HStack {
                        Image(systemName: "phone.badge.checkmark")
                        Text("Send Push to Device ID")
                    }
                    .frame(maxWidth: .infinity)
                    .padding()
                    .background(Color.purple)
                    .foregroundStyle(.white)
                    .cornerRadius(8)
                }

                Button(action: {
                    appDelegate.sendPushToClientId()
                    statusMessage = "Sending push to client ID..."
                }) {
                    HStack {
                        Image(systemName: "person.crop.circle.badge.checkmark")
                        Text("Send Push to Client ID")
                    }
                    .frame(maxWidth: .infinity)
                    .padding()
                    .background(Color.orange)
                    .foregroundStyle(.white)
                    .cornerRadius(8)
                }

                HStack(spacing: 8) {
                    Menu {
                        Button(titleForChannel("exampleChannel1")) {
                            selectedChannel = "exampleChannel1"
                        }
                        Button(titleForChannel("exampleChannel2")) {
                            selectedChannel = "exampleChannel2"
                        }
                    } label: {
                        HStack {
                            Image(systemName: "line.3.horizontal.decrease.circle")
                            Text(titleForChannel(selectedChannel))
                        }
                        .frame(maxWidth: .infinity)
                        .padding()
                        .background(Color.gray.opacity(0.2))
                        .cornerRadius(8)
                    }

                    Button(action: {
                        appDelegate.sendPushToChannel(selectedChannel)
                        statusMessage = "Sending push to channel: \(selectedChannel)..."
                    }) {
                        HStack {
                            Image(systemName: "checkmark.circle.fill")
                            Text("Send")
                        }
                        .frame(maxWidth: .infinity)
                        .padding()
                        .background(Color.cyan)
                        .foregroundStyle(.white)
                        .cornerRadius(8)
                    }
                }
            }
        }
        .padding()
        .background(Color.white)
        .cornerRadius(12)
        .shadow(radius: 2)
    }
}

Update your ContentView body to include this section (add it just after the ChannelSection):

Swift

1

2

// Send Push Section
SendPushSection(appDelegate: appDelegate, statusMessage: $statusMessage, selectedChannel: $selectedChannel)

Build and run your app again. Use the added section to send push notifications.

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 (e.g., 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)
    }
}

Receiving 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.

Next steps

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