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

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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

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

// Modify the notification content here...
bestAttemptContent.title = "\(bestAttemptContent.title) [modified]"
bestAttemptContent.title = ROT13.shared.decrypt(bestAttemptContent.title)
bestAttemptContent.body = ROT13.shared.decrypt(bestAttemptContent.body)
$ php sendEncrypted.php

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.

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

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

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?

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

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

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.

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
}()

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

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 accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now