Adding a content extension to your app

The last feature of notifications that you will learn about in this chapter is a feature that can really make your notification experience pop. Content extensions enable developers to take custom notification actions to a whole new level. You already know that 3D-Touching a notification will make custom actions pop up. However, the notification maintains a standard look and feel, which might not be exactly what you want.

Consider receiving an invite for an event. The notification allows you to accept the invite, or decline it. Wouldn't it be great if your calendar popped up inside that notification as well, allowing you to check your calendar without having to actually open your calendar app, before responding to the invite? This is what content extensions are for. When implemented correctly, they can provide users with essential information relevant to the notification that's currently on display.

To demonstrate the possibilities for content extensions, you will use the simple notification you created for the Notifications app, and take it to the next level. Earlier, you scheduled a local notification that simply notified the user about a reminder. The notification itself contained only the notification title and custom actions that allow the user to mark a task as finished.

You will now use a content extension to show more context about the reminder. The content extension will show the user when the notification was due, and whether it is currently pending. In addition to this, you will also update the notification UI when the user taps the Complete action, so the UI reflects the current reminder status. If a notification is no longer pending by the time the notification is delivered, the Complete action will be replaced with a Pending action.

Get started by adding a new extension to your project, and select the Notification Content Extension type. Make sure to add your extension to the shared App Group you have set up before, and add the Core Data model definition and PersistentHelper to the extension target.

When you add a content extension to your project, Xcode generates a small sample implementation for you, which you can use as a reference to see a minimal working implementation for a content extension. Open the NotificationViewController class and have a quick look at its content, before removing all generated code and adding the following outlets:

@IBOutlet var reminderTitleLabel: UILabel!
@IBOutlet var reminderDueDateLabel: UILabel!
@IBOutlet var reminderStatusLabel: UILabel!

Next, add the following implementation for didReceive(_:) to set the correct values on the outlets you added:

func didReceive(_ notification: UNNotification) {
  guard let identifier = notification.request.content.userInfo["reminder-uuid"] as? String
    else { return }

  let predicate = NSPredicate(format: "identifier == %@", identifier)
  let fetchRequest: NSFetchRequest<Reminder> = Reminder.fetchRequest()
  fetchRequest.predicate = predicate

  let moc = PersistentHelper.persistentContainer.viewContext
  guard let results = try? moc.fetch(fetchRequest),
    let reminder = results.first
    else { return }

  let dateFormatter = DateFormatter()
  dateFormatter.dateStyle = .long
  dateFormatter.timeStyle = .medium

  reminderTitleLabel.text = reminder.title
  reminderDueDateLabel.text = dateFormatter.string(from: reminder.dueDate!)
  reminderStatusLabel.text = reminder.isCompleted ? "Done" : "Pending"
}

In the storyboard for the content extension, remove the existing label and add three new labels, one for the reminder title, one for the due date, and one for the reminder status. Don't forget to connect the labels to their corresponding outlets, and remove the original outlet that was present in the storyboard. Giving the view controller a height of ±100 points will help you position the labels more easily. You can use stack views and Auto Layout to replicate the following screenshot:

Finally, open the content extension's Info.plist file. As mentioned before, content extensions are associated with notifications through categories. The Info.plist file is used to specify the category that the current extension should be associated with. Expand the NSExtension and NSExtensionAttributes properties to find the UNNotificationExtensionCategory property. Give this field a value of reminder.

You are now ready to take your extension for a spin. Select your extension from the drop-down menu next to the run and stop controls in the top-left corner of Xcode, and run your project. Xcode will ask you for an app to run; pick Notifications. Schedule a new reminder in the Notifications app and wait for a notification to appear.

Once the notification appears, the following UI should be shown when you 3D-Touch on the notification:

If you've tested this example yourself, you may have noticed that the notification's custom view was too high initially, and that it animated to the correct size for properly fitting your notification contents. The notification determines its final size using AutoLayout, so the final size won't be known until the notification content is on screen. The animation doesn't look very good, but luckily, there is a way to minimize the amount of resizing that your content extension has to do.

In the extension's Info.plist file, there is a property called UNNotificationExtensionInitialContentSizeRatio, which sits right below the notification category property. The default value for this property is 1, but for your notification, you could probably use a value that's a lot smaller. Try setting this value to 0.2 and run your extension again. It doesn't have to animate nearly as much because the initial height is now just 20% of the extension's width. Much better.

You probably also noticed that the original notification contents are visible below the custom view. You can hide this default content by adding the UNNotificationExtensionDefaultContentHidden property to the extension's Info.plist file. All the properties you add for your extension should be added at the same level as UNNotificationExtensionInitialContentSizeRatio and UNNotificationExtensionCategory:

This wraps up the interface part of the content extension. In addition to showing a custom UI for a notification, content extensions can respond to actions that the user selects right inside the content extension itself. To do this, you should implement the didReceive(_:completionHandler:) delegate method in the extension. Once this method is implemented, the extension becomes responsible for all actions that are chosen by the user. This means that the extension should either handle all the actions, or that it should explicitly pass them on to the host app if the selected action can't be handled within the content extension.

After handling the incoming action, the notification extension determines what should happen next. This is done by calling the completion handler with a UNNotificationContentExtensionResponseOption. There are three options to choose from:

  • Dismiss the notification and do nothing
  • Dismiss the notification and forward the chosen action to the host app
  • Keep the extension active so the user can pick more actions or, using Apple's Messages app as an example, so that the user can carry on the conversation inside the content extension

Add the following implementation for didReceive(_:completionHandler:) to mark a reminder as completed when a user taps the Complete action:

func didReceive(_ response: UNNotificationResponse, completionHandler completion: @escaping (UNNotificationContentExtensionResponseOption) -> Void) {

  guard let reminder = extractReminderFromResponse(response) else {
    completion(.dismissAndForwardAction)
    return
  }

  if response.actionIdentifier == "complete-reminder" {
    setCompleted(true, forReminder: reminder, completionHandler: completion)
  } else {
    setCompleted(false, forReminder: reminder, completionHandler: completion)
  }
}

func setCompleted(_ completed: Bool, forReminder reminder: Reminder, completionHandler completion: @escaping (UNNotificationContentExtensionResponseOption) -> Void) {

  reminder.managedObjectContext!.perform {
    reminder.isCompleted = true
    self.reminderStatusLabel.text = reminder.isCompleted ? "Done" : "Pending"

    try! reminder.managedObjectContext!.save()

    completion(.doNotDismiss)
  }
}

func extractReminderFromResponse(_ response: UNNotificationResponse) -> Reminder? {
  let userInfo = response.notification.request.content.userInfo

  guard let identifier = userInfo["reminder-uuid"] as? String else {
      return nil
  }

  let fetchRequest: NSFetchRequest<Reminder> = Reminder.fetchRequest()
  fetchRequest.predicate = NSPredicate(format: "identifier == %@", identifier)

  let moc = PersistentHelper.persistentContainer.viewContext
  guard let results = try? moc.fetch(fetchRequest),
    let reminder = results.first else {
      return nil
  }

  return reminder
}

The preceding code takes very similar approach to marking the reminder as completed to the one you implemented earlier in AppDelegate. Note that the reminder can also be marked as not completed. The reason this is taken into account is that once the user has tapped the Complete action, this action should be swapped out for a Pending action, in case the user marked the reminder as completed by accident.

Add the following code to replace the action that is shown underneath the content extension:

func setNotificationForReminder(_ reminder: Reminder) {
  let identifier = reminder.isCompleted ? "pending-reminder": "complete-reminder"
  let label = reminder.isCompleted ? "Pending" : "Complete"

  let action = UNNotificationAction(identifier: identifier, title: label, options: [])
  extensionContext?.notificationActions = [action]
}

The preceding method creates and applies a new action, depending on the current reminder status. Add calls to this method after setting the current reminder's completion status and in the initial didReceive(_:) method, so that the correct action is shown in the event that the reminder associated with the notification has already been completed.

As you've seen just in the preceding examples, content extensions are not complex to implement, yet they are amazingly powerful. You can add an entirely new layer of interaction and increase the relevance of notifications by implementing a content extension, and it is strongly recommended that you always consider ways to implement extensions for your notifications to provide the user with rich information about their notification as quickly as possible.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset