Chapters

Hide chapters

Push Notifications by Tutorials

Fourth Edition · iOS 16 · Swift 5 · Xcode 14

Section I: Push Notifications by Tutorials

Section 1: 15 chapters
Show chapters Hide chapters

10. Modifying the Payload
Written by Scott Grosch

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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 they have 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 set the team signing as discussed in Chapter 7, “Expanding the Application.”

Gibberish

Build and run your app, and send yourself a push notification with the following payload:

{
  "aps": {
    "alert": {
      "title": "Lbhe Gnetrg",
      "body": "Guvf vf lbhe arkg nffvtazrag."
    },
    "sound": "default",
    "badge": 1,
    "mutable-content": 1
  },
  "media-url":
          "uggcf://jbyirevar.enljraqreyvpu.pbz/obbxf/abg/ohaal.zc4"
}

Creating the Service Extension

You need to add a service extension target so that you can handle the encryption being used.

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.

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 })
  }
}
// Modify the notification content here...
bestAttemptContent.title = "\(bestAttemptContent.title) [modified]"
bestAttemptContent.title = ROT13.shared.decrypt(bestAttemptContent.title)
bestAttemptContent.body = ROT13.shared.decrypt(bestAttemptContent.body)

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

guard let bestAttemptContent else {
  return
}

bestAttemptContent.title = ROT13.shared.decrypt(bestAttemptContent.title)
bestAttemptContent.body = ROT13.shared.decrypt(bestAttemptContent.body)
guard
  let urlPath = request.content.userInfo["media-url"] as? String,
  let url = URL(string: ROT13.shared.decrypt(urlPath))
else {
  contentHandler(bestAttemptContent)
  return
}
Task {
  // 1
  defer { contentHandler(bestAttemptContent) }

  do {
    // 2
    let (data, response) = try await URLSession.shared.data(from: url)
    // 3
    let file = response.suggestedFilename ?? url.lastPathComponent
    let destination = URL(fileURLWithPath: NSTemporaryDirectory())
      .appendingPathComponent(file)
    try data.write(to: destination)
    // 4
    let attachment = try UNNotificationAttachment(
          identifier: "",
          url: destination)
    bestAttemptContent.attachments = [attachment]
    contentHandler(bestAttemptContent)
  } catch {
    // 5
    print("An error occurred.")
  }
}

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.

Sharing Data With Your Main Target

Your primary app target and your extension are two separate processes. You 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.

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 provide 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?

import Foundation

extension UserDefaults {
  // 1
  static let suiteName = "group.com.yourcompany.PushNotifications"
  static let extensions = UserDefaults(suiteName: suiteName)!
  // 2
  private enum Keys {
    static let badge = "badge"
  }
  // 3
  var badge: Int {
    get { UserDefaults.extensions.integer(forKey: Keys.badge) }
    set { UserDefaults.extensions.set(newValue, forKey: Keys.badge) }
  }
}

if let increment = bestAttemptContent.badge as? Int {
  if increment == 0 {
    UserDefaults.extensions.badge = 0
    bestAttemptContent.badge = 0
  } else {
    let current = UserDefaults.extensions.badge
    let new = current + increment

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

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.

let url: URL
if inMemory {
  url = URL(fileURLWithPath: "/dev/null")
} else {
  let groupName = "group.com.yourcompany.PushNotifications"
  url = FileManager.default
    .containerURL(forSecurityApplicationGroupIdentifier: groupName)!
    .appendingPathComponent("PushNotifications.sqlite")
}

container.persistentStoreDescriptions.first!.url = url

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.

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.

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 to download videos or other content from the internet. Once downloaded, 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 default. You can overcome this using App 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 App 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.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now