Chapters

Hide chapters

Push Notifications by Tutorials

Second Edition · iOS 13 · Swift 5.1 · Xcode 11

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section I: Push Notifications by Tutorials

Section 1: 14 chapters
Show chapters Hide chapters

10. Modifying the Payload
Written by Scott Grosch

Sometimes, you’ll need to take extra steps before a notification is presented to the user. For example, you may wish to download an image or change the text of a notification.

In the DidIWin lottery app, for example, you’d want the notification to tell the user exactly how much money he or she has won. Given the push notification simply contains today’s drawing numbers, you’ll be using a Notification Service Extension to intercept those numbers and apply logic to them.

You can think of a Notification Service Extension as middleware between APNs and your UI. With it, you can receive a remote notification and modify its content before it’s presented to the user. Considering the fact notification payloads are limited in size, this can be a very useful trick! Another common use case for modifying the payload is if you’re sending encrypted data to your app. The service extension is where you’d decrypt the data so that it’s properly displayed to your end user.

In this chapter, you’ll go over what it takes to build a Notification Service app extension and how to implement some of its most common use cases.

Configuring Xcode for a service extension

Due to your proven track record of writing amazing apps, your country’s spy agency has contracted you to write the app that its field agents will use to receive updates from headquarters. Of course, the agency sends all of its data using massive encryption, so you’ll need to handle the decryption for the agents. Nobody wants to read a gobbledygook text!

Open the starter project for this chapter. Remember to turn on the Push Notifications capability as discussed in Chapter 4, “Xcode Project Setup,” and set the team signing as discussed in Chapter 7, “Expanding the Application.”

Edit AppDelegate.swift to contain your IP address in the call to sendPushNotificationDetails(to:using:).

In order to find out your IP address, go into System PreferencesNetworkAdvancedTCP/IP and copy the value under IPv4 Address. Paste this value between http:// and :8080 in the code, like so: http://YOUR_IP_HERE:8080/api/token.

Now, you need to add your extension target so that you can handle the encryption being used.

  1. In Xcode, select FileNewTarget….
  2. Make sure iOS is selected and choose the Notification Service Extension.
  3. For the product name specify Payload Modification.
  4. Press Finish.
  5. If asked about scheme activation, select Cancel.

Note: You don’t actually run a service extension so that’s why you didn’t let it make the new target your active scheme.

You can name the new target anything that makes sense for you, but it can be helpful to use the above name because, when you glance at your project, you will immediately know what that target is doing.

If you look in the Project navigator ( + 1), you’ll see you now have a new folder group called Payload Modification. You’ll notice that there’s a NotificationService.swift file but no storyboard. This is because service extensions don’t present any type of UI. They are called before the UI is presented, be it yours or the one Apple displays for you. You’ll get into UI modifications in the next chapter.

Open the NotificationService.swift file and you’ll see a bit of content already provided for you by Apple. The first method in this file, didReceive(_:withContentHandler:) is called when your notification arrives. You have roughly 30 seconds to perform whatever actions you need to take. If you run out of time, iOS will call the second method, serviceExtensionTimeWillExpire to give you one last chance to hurry up and finish.

If you’re using a restartable network connection, the second method might give you just enough time to finish. Don’t try to perform the same actions again in the serviceExtensionTimeWillExpire method though. The intent of this method is that you perform a much smaller change that can happen quickly. You may have a slow network connection, for example, so there’s no point in trying yet another network download. Instead, it might be a good idea to tell the user that they got a new image or a new video, even if you didn’t get a change to download it.

Note: If you haven’t called the completion handler before time runs out, iOS will continue on with the original payload.

You may make any modification to the payload you want — except for one. You may not remove the alert text. If you don’t have alert text, then iOS will ignore your modifications and proceed with the original payload.

Decrypting the payload

As mentioned at the start of the chapter, the payload you receive has encrypted the data. Your country is a little bit behind the times though, and it is still using the ROT13 letter substitution cipher in which each letter is simply replaced by the letter 13 places further along in the alphabet, wrapping back to the beginning of the alphabet if necessary.

Implementing the ROT13 cipher

In your Payload Modification target create a new Swift file named ROT13.swift and paste this code into it:

import Foundation

struct ROT13 {
  static let shared = ROT13()

  private let upper = Array("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
  private let lower = Array("abcdefghijklmnopqrstuvwxyz")
  private var mapped: [Character: Character] = [:]

  private init() {
    for i in 0 ..< 26 {
      let idx = (i + 13) % 26
      mapped[upper[i]] = upper[idx]
      mapped[lower[i]] = lower[idx]
    }
  }

  public func decrypt(_ str: String) -> String {
    return String(str.map { mapped[$0] ?? $0 })
  }
}

You can find many different ways of implementing this cipher in Swift. The above is just a quick and dirty way to handle the American English alphabet.

Obviously, the above code makes a nice sample for a book as it doesn’t require downloads and configuration. However, for real security, you should look to something like the CryptoSwift (cryptoswift.io) library.

Modifying the payload

Run your app on a device, taking note of the device token that gets printed to the console window. In the starter materials for this project, you’ll find a sendEncrypted.php file. Edit this file with your favorite text editor and specify your token and other details at the top of the file. When done, run it from Terminal:

$ php sendEncrypted.php

Note: If you haven’t already set up the sendEncrypted.php script, you can find instructions on how to set it up in Chapter 6, “Server Side Pushes.”

If everything goes correctly, you should see a notification on your device. However, this notification is encrypted by the agency, and you need to decrypt the contents before displaying the notification on the device.

Now, back in your NotificationService.swift file, find the lines in didReceive that show an example modification:

// Modify the notification content here...
bestAttemptContent.title = "\(bestAttemptContent.title) [modified]"

Replace them with code to decrypt the data:

bestAttemptContent.title = ROT13.shared.decrypt(bestAttemptContent.title)
bestAttemptContent.body = ROT13.shared.decrypt(bestAttemptContent.body)

Build and run your app again, and then go back to Terminal and run the PHP script again:

$ php sendEncrypted.php

If everything worked correctly, you should see a decrypted push notification appear on your phone.

Downloading a video

Service extensions are also the place in which you can download videos or other content from the internet. First, you need to find the URL of the attached media. Once you have that, you can try to download it into a temporary directory somewhere on the user’s device. Once you have the data, you can create a UNNotificationAttachment object, which you can attach to the actual notifcation.

Go back to NotificationService.swift and, right after you decrypt the body of the message, before the call to the contentHandler closure, add the following code to download any video they might have sent along:

// 1
if let urlPath = request.content.userInfo["media-url"] as? String,
   let url = URL(string: ROT13.shared.decrypt(urlPath)) {
  // 2
  let destination = URL(fileURLWithPath: NSTemporaryDirectory())
      .appendingPathComponent(url.lastPathComponent)

  do {
    // 3
    let data = try Data(contentsOf: url)
    try data.write(to: destination)

    // 4
    let attachment = try UNNotificationAttachment(
      identifier: "",
      url: destination)

    // 5
    bestAttemptContent.attachments = [attachment]
  } catch {
    // 6
  }
}

Here’s what the above code is doing:

  1. You first have to make sure that not only did they send along a media-url key but that you can turn it into a valid URL. Don’t forget to also decrypt the URL! You don’t want those foreign operatives knowing your URLs!
  2. You’ll also make a local file URL where you’ll write the data to. Ensure that your filename stays the same so that iOS knows what type of file you’re working with. If you save to a file with a random extension, your download isn’t going to work the way you are expecting.
  3. Use the Data(contentsOf:) method to perform a synchronous data download. You can’t use an asynchronous method or the function will exit before your data has been retrieved.
  4. Once you’ve written the data to disk, you just create a UNNotificationAttachment. iOS will generate a unique identifier for you if you leave it empty.
  5. Finally, add the attachment to the content of the push notification.
  6. There’s not really anything you can do if the download fails, so you’ll just be leaving the error case empty.

Build and run your app again, and then rerun the sendEncrypted.php script.

You should get a push notification that has a small image on the right-hand side. Long-press the notification and you’ll see a video with your next target!

Service extension payloads

You don’t necessarily always want an extension to run every time you receive a push notification — just when it needs to be modified. In the above example, you’d obviously use it 100% of the time as you’re decrypting data. But what if you were just downloading a video? You don’t always send videos.

To tell iOS that the service extension should be used, simply add a mutable-content key to the aps dictionary with an integer value of 1. You’ll notice that the provided sendEncrypted.php already includes this key for you.

Note: If you forget to add this key, your service extension will never be called. You’re most likely going to forget to do this and have a heck of a time figuring out why your code doesn’t work!

Sharing data with your main target

Your primary app target and your extension are two separate processes. They can’t share data between them by default. If you do more than the most simplistic of things with your extension, you’ll quickly find yourself wanting to be able to pass data back and forth. This is easily accomplished via Application Groups, which allows access to group containers that are shared between multiple related apps and extensions.

To enable this capability, press + 1 to go back to the Project navigator and click on your main target. Next, navigate to the Signing & Capabilities tab again. Click the + Capability button in the top-left and you’ll see App Groups near the top of the list. Double-click it.

You should see a new section pop up called App Groups in the tab. Press the + button and then set the name you wish to use. Generally, you’ll want the same name as your bundle identifier, just prefixed with group:

You’ll notice, in this image, that an App Group has already been created for another project, so that’s also shown in the image. Be sure that you only select the one group you want if there are multiple listed.

Now, go into your Payload Modification target’s capabilities tab and enable the App Groups there as well, selecting the same app group you selected for your app target.

Badging the app icon

A great use for service extensions is to handle the app badge. As discussed in Chapter 3, “Remote Notification Payload,” iOS will set the badge to exactly what you specify in the payload, if you provider a number. What happens if the end user has ignored your notifications so far? Maybe you’ve sent them three new items at this point. You’d rather the badge said 3 and not 1, right?

Historically, app developers have sent information back to the server as to how many badges the app icon is currently displaying, and then the push notification would increment that number by one. While that’s doable, it’s quite a bit of extra overhead to deal with on your server. By utilizing a service extension, you can now just pretend that the badge key being there means to increment the badge count by that number. You’re now just storing locally how many items are unread versus having to send those details back to your server for tracking.

As this is just an integer value, you can make use of the UserDefaults class with one small change — assuming you’ve already enabled App Groups. You have to specify the suite that is used to enable it to span targets. To do so, add a new Swift file to your primary target, not the extension, called UserDefaults.swift:

import Foundation

extension UserDefaults {
  // 1
  static let suiteName = "group.com.raywenderlich.PushNotifications"
  static let extensions = UserDefaults(suiteName: suiteName)!

  // 2
  private enum Keys {
    static let badge = "badge"
  }

  // 3
  var badge: Int {
    get {
      return UserDefaults.extensions.integer(forKey: Keys.badge)
    }

    set {
      UserDefaults.extensions.set(newValue, forKey: Keys.badge)
    }
  }
}
  1. First, you define a new extensions property, providing a UserDefaults object you’d use when you want to share your defaults between targets.
  2. Hardcoding strings is a bad idea, so you create an enum with a static let so that you only have to do it once. A struct would work here just as well. The reason you want to use an enum is that you can’t accidentally instantiate it.
  3. Finally, you wrap up by creating a computed property for badge that handles the get/set. Again, this is just good coding style to make life easier on the caller.

Right now, this file is only accessible from the main target though. Bring up the File inspector by pressing + + 1 and in the Target Membership section check the boxes next to your service extension as well as the primary target:

Now, back in NotificationService.swift, edit the didReceive(_:withContentHandler:) method. You can check for badging information by placing the following code just after the section where you download the video, right before the call to contentHandler:

if let incr = bestAttemptContent.badge as? Int {
  switch incr {
  case 0:
    UserDefaults.extensions.badge = 0
    bestAttemptContent.badge = 0
  default:
    let current = UserDefaults.extensions.badge
    let new = current + incr

    UserDefaults.extensions.badge = new
    bestAttemptContent.badge = NSNumber(value: new)
  }
}

It’s important to store the value to a UserDefaults type structure so you modify that value in your primary target as well. When your user accesses the part of your app that the badge refers to, you’ll want to decrement the badge count so that the app icon is updated.

Build and run the app. From Terminal, run the sendEncrypted.php script a few times. The badge number should increase for each notification you recieve:

Accessing Core Data

Writing to a UserDefaults key can be incredibly useful, but isn’t normally good enough. Sometimes, you really just need access to your actual app’s data store in your extension. Most commonly, you’ll look for a way to access Core Data. It’s easy enough to do once you’ve enabled App Groups.

First, select your data model (PushNotifications.xcdatamodeld). Then, in the Target Membership section of the File inspector, add a checkmark next to your service notification target. If you created any NSManagedObject subclasses that you need to use, do the same thing with them.

Second, edit your AppDelegate.swift file, making a small change to the persistentContainer lazy variable. You’ve got to tell the container exactly where to store the data. So modify the default like so:

lazy private var persistentContainer: NSPersistentContainer = {
  // 1
  let groupName = "group.YOUR_BUNDLE_ID"
  let url = FileManager.default
    .containerURL(forSecurityApplicationGroupIdentifier: groupName)!
    .appendingPathComponent("PushNotifications.sqlite")
   
  // 2
  let container = NSPersistentContainer(name: "PushNotifications")
  
  // 3
  container.persistentStoreDescriptions = [
    NSPersistentStoreDescription(url: url)
  ]

  // 4
  container.loadPersistentStores(completionHandler: { 
    _, error in
    
    if let error = error as NSError? {
      fatalError("Unresolved error \(error), \(error.userInfo)")
    }
  })
        
  return container
}()

Using app groups with Core Data really only requires two changes to the default setup.

  1. The first difference is that you have to tell iOS exactly where to write the internal .sqlite file since the default doesn’t work with app groups. Be sure you use the exact name that you gave the App Group!
  2. Creating the container is no different than the default setup.
  3. However, the container has to know that it’s using your custom location.
  4. Then, just load the store like normal and return the container.

Now, your main app knows exactly where to write the Core Data database to, but your extension still doesn’t have this information.

Copy the lazily computed property you just used exactly as is and then paste it into Payload Modification/NotificationService.swift file, inside of the NotificationService class. Remember to add an import CoreData statement or you’ll get a build error. Now, you can access your data model in the extension.

Localization

If you’re modifying the content of your payload, you might be modifying the text as well. Always keep in mind that not everyone speaks the same language you do, so you still need to follow all the localization rules you normally would.

Note: There’s currently a bug in Xcode in which your base language will not always be used in an extension. To work around this bug, simply make sure that you have a Localizable.strings for your base language defined.

If the only reason you’re using an extension is to perform localizations on text, you should instead look at the keys of the aps alert dictionary, as explained back in Chapter 3, “Remote Notification Payload,” as there are multiple items there to perform this action for you.

Debugging

Sometimes, no matter how hard you try, things just don’t go right. Debugging a service extension works almost the same as any other Xcode project. However, because it’s a target and not an app, you have to take a few extra steps.

  1. Open up your NotificationService.swift file and set a breakpoint on the line where you decode the title.

  2. Build and run your app.

  3. In Xcode’s menu bar, choose DebugAttach to Process by PID or Name….

  4. In the dialog window that appears, enter Payload Modification — or whatever you named your target.

  5. Press the Attach button.

If you send yourself another push notification, Xcode should stop execution at the breakpoint you set. Be aware that debugging service extensions is a bit finicky and sometimes it just plain doesn’t work. If you aren’t able to find your process listed, you might have to go through a full restart of Xcode and possibly even a reboot of your device.

Key points

  • A Notification Service Extension is a sort of middleware between APNs and your UI. With it, you can receive a remote notification and modify its content before it’s presented to the user.

  • You may make any modification to the payload you want — except for one. You may not remove the alert text. If you don’t have alert text, then iOS will ignore your modifications and proceed with the original payload.

  • You can use service extensions so that you can download videos or other content from the internet; you will create a UNNotificationAttachment object that you attach to the push notification.

  • Your primary app target and your extension are two separate processes and cannot share data between them by defualt. You can overcome this using Application Groups.

  • Service extensions can be used to handle your app’s badge so that the badge reflects the number of unseen notifications without having to involve server side storage.

  • You can access your app’s data store in your extension once you have Application Groups set up.

  • When modifying the content of your payload, if your text is also changed, follow localization rules to account for different languages.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.