Active Filters: Notifications

We're finishing up our extensive look into the new Notifications improvements in iOS 10 today with Notification Service Extensions. These fill out the notifications functionality by allowing us to intercept remote notifications as they are received on the device, take some action(s), and then modify the notification before it's shown to the user. Let's get started!

We'll begin by looking at the broad concept going on here. Until iOS 10, Remote Notifications were presented to the user as soon as they arrived, without any interaction from their app.

In iOS 10, Apple has provided a mechanism for us to actually "intercept" Remote Notifications when they arrive, do some work, modify the notification's content, then send it along to be displayed to the user.

The whole process now goes like this:

This new step opens up a ton of possibilites and gives us a chance to do things like download media, or perform some other short work to enrich the notification before the user sees it.

Let's try this out.

First, we'll add another new target to our app, and choose a Notification Service Extension:

In the new group that was created we'll find one file, a subclass of UNNotificationServiceExtension.

Notification Service Extensions run in the background, and never show any kind of interface themselves. Instead, they override one function that's called whenever a notification is received.

We're passed in a notification request and a completion closure.

The request is the same UNNotificationRequest type we first learned about in Bite #258, only with its trigger property set to a UNPushNotificationTrigger, neat!

We'll perform whatever work we need, grab the content from the the notification request's, modify its properties, and then send it along in the completion closure:

class NotificationService: UNNotificationServiceExtension {
  override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
    guard let asteroidID = request.content.userInfo["asteroid-id"] as? String else { return }
    guard let mutableContent = request.content.mutableCopy() as? UNMutableNotificationContent else { return }

    AsteroidService.details(asteroidID: asteroidID) {
      mutableContent.body
        .append(" Estimated Mass: \($0.massInTons) tons")

      contentHandler(mutableContent)
    }
  }
}

Here we take advantage of the custom "userInfo" dictionary our server adds to the push notification payload. In this case, we're grabbing an asteroidID from it. We also grab a mutable instance of the notification's content so we can modify it later.

Then we load the details for the asteroid, and append a new piece of information to the notification's body, the estimated mass.

Finally, we send the modified content along in the contentHandler.

We don't need it here, but there's one more function we can override, and it's all about expiration.

Unlike some other more generous extension points, Notification Service Extensions are given an extremely short window to perform work and complete by the system.

We can override:

override func serviceExtensionTimeWillExpire() {

}

This will tell us when the system is about to forcibly stop our extension, if this happens, the system will display the notification using whatever push notification payload our server sent through, with no modifications.

In our example above, we lean on this fallback behavior, so we didn't need to override this function.

Finally, we'll push an example notification through using Knuff, (covered in Bite #177), and try this all out:

Success!

We've been looking at the various improvements in Notifications in iOS 10, and today we're trying out one of the coolest new features: Notification Content Extensions. These allow us to display completely custom content when the user drags down or 3D Touches on one of our app's notifications. Let's dive in.

We'll start by adding a new target to our app and choosing Notification Content Extension.

We'll give it a name, and check out the files we get.

Like many of the other extension types we've covered, the whole thing is basically a view controller, storyboard, and Info.plist.

We'll head to the Info.plist first and configure a few things.

Since we want our custom content to be all that's visible once the users opens up our notification, we've added the UNNotificationExtensionDefaultContentHidden key hide the title and body text labels that are visible before open

We've also set our notification category to match the one we'll send in our UNNotificationContent.

Before we continue, let's add some actions to our notification so the user can actually do something about it. We've covered Notification Actions in the past (Bite #122), but here's how things work in the User Notifications Framework world:

// Somewhere before we request our first notification:
let evade = UNNotificationAction(
  identifier: "evade",
  title: "Evade with Autopilot πŸ€–"
)
let destroy = UNNotificationAction(
  identifier: "destroy",
  title: "Destroy (Weapons to Maximum!) ",
  options: [.destructive]
)
let category = UNNotificationCategory(
  identifier: "asteroid-detected",
  actions: [evade, destroy],
  intentIdentifiers: []
)

UNUserNotificationCenter.current().setNotificationCategories([category])

Now, we can configure a notification just like we have before:

let notification = UNMutableNotificationContent()

notification.categoryIdentifier = "asteroid-detected"

notification.title = "Asteroid Detected! πŸš€"
notification.body = "Collision imminent. Immediate action required!"

Then we'll send off a request to the notification center to show the notification in 2 seconds. (Enough time for us to press the home button and head out to the home screen).

let request = UNNotificationRequest(
  identifier: "asteroid-id-123",
  content: notification,
  trigger: UNTimeIntervalNotificationTrigger(
    timeInterval: 2,
    repeats: false
  )
)

UNUserNotificationCenter.current().add(request)

Nice, so far so good. Now, let's see what happens when we open it up (either by 3D Touching on a supported device, or simply dragging down the banner):

Success! Our custom content is shown inside the notification, along with our custom actions.

Topics

#261: Rich Notifications πŸ’°πŸ’­

Topics

Today we'll continue our look at Notifications in iOS 10 by looking at the new "rich" attributes we can use to give our notifications titles, subtitles, and even media! Let's check it out.

Most of this code will center around our old friend UNNotificationContent:

let notification = UNMutableNotificationContent()

notification.categoryIdentifier = "asteroid-detected"

notification.title = "Asteroid Detected! πŸš€"
notification.body = "Collision imminent. Immediate action required!"

We're already using two new fields here: title and body. This allows us to properly highlight the important and extra information in our notification's UI:

Nice.

Now, it'd be really great if we could show the user an image of the giant space rock they're hurdling towards. Let's attach some media to do this:

if let attachment = try? UNNotificationAttachment(
  identifier: "asteroid-id-123-photo",
  url: asteroidImageURL
) {
  notification.attachments.append(attachment)
}

Heads up: That will need to be a local file URL, not a remote one.

Now, when our notification is shown, we see a nice little preview of the asteroid:

One small note here: The thumbnail image shown here is the result of adding padding manually to the image to account for the the system's aggressive cropping.

There is a way to pass a dictionary of options to UNNotificationAttachement. There's even a key called UNNotificationAttachmentOptionsThumbnailClippingRectKey.

This key requires a "normalized" CGRect describing how the image should be clipped.

Setting this key to CGRect(x: 0, y: 0, width: 1, height: 1) should allow the whole thing to be visible, but it seems the system ignores this value entirely as of Beta 6.

OpenRadar #27708976, rdar://27708976

Topics

#260: New Notifications Capabilities πŸ’­

Topics

Today we'll continue our look at Notifications in iOS 10 by checking out some new capabilities for interacting with the user's notification preferences, and also the notifications we've requested and setup with the notification center. Let's begin.

First up, we finally have a way to "ask" the system for the user's current notification preferences state:

UNUserNotificationCenter.current()
  .getNotificationSettings { settings in 
    // settings is a beautifully descriptive
    // UNNotificationSettings object.
  }

Inside this fancy UNNotificationSettings object is a wealth of detail about exactly how the user has configured our app's notifications to work. Everything from authorizationStatus to alertStyle. Neat!

Next up, since we've been requesting all these notifications to be triggered, we can now ask the system to give us the list of pending notifications. (Notifications that are scheduled, but haven't been displayed yet).

UNUserNotificationCenter.current()
  .getPendingNotificationRequests { requests in
    // requests is an array
    // of UNNotificationRequest objects.
  }

From here, we can take the identifier of any of these requests, and use it to (for example) "unschedule" it:

UNUserNotificationCenter.current()
  .removePendingNotificationRequests(
    withIdentifiers: ["asteroid-id-123"]
  )

Functions also exist to remove all pending notifications at once.

Last but certainly not least, we can actually do this exact same type of management on notifications that have already been shown to the user.

That's right we can retrieve the notifications that are currently in Notification Center like this:

UNUserNotificationCenter.current()
  .getDeliveredNotifications { (notifications) in
    _ = notifications.map { print($0.request.identifier) }
  }

We can also remove any or all of these notifications by identifier as well. Super neat!

Topics

#259: Notification Triggers πŸ’­

Topics

Continuing our look at Notifications in iOS 10, today we'll check out the different notification triggers we can use to display notifications in our app. Let's get started.

We'll use the same standard authorization and testbed setup we did in Bite #258, only this time, we'll create some triggers.

We'll compose a quick UNNotificationContent:

let notification = UNMutableNotificationContent()

notification.title = "Entered Mothership!"
notification.body = "Welcome to Apple."

Now, we'll start trying out triggers. First, let's look at a location trigger:

let region = CLCircularRegion(
  center: CLLocationCoordinate2DMake(
    37.3278964, 
    -122.0215575
  ),
  radius: 100.0,
  identifier: "Mothership"
)

region.notifyOnExit = false

let trigger = UNLocationNotificationTrigger(
  region: region, 
  repeats: false
)

This will cause our notification to be displayed when the user enters the area around Apple's new Spaceship campus. Neat!

Then, we'll hand everything off to the notification center via a UNNotificationRequest, just as we did before:

UNUserNotificationCenter.current().add(
  UNNotificationRequest(
    identifier: "mothership-1",
    content: notification,
    trigger: trigger
  )
)

When it comes to time-based triggers, we're given a couple of options.

We'll compose another quick UNNotificationContent:

let notification = UNMutableNotificationContent()

notification.title = "WAKE UP!"
notification.body = "You're late for the Mothership!"

There's UNCalendarNotificationTrigger. This allows us to specify only the components we want to trigger on. For example, here we're creating a trigger that will repeat every morning at 7:15 AM:

let trigger = UNCalendarNotificationTrigger(
  dateMatching: DateComponents(hour: 7, minute: 15),
  repeats: true
)

We can also easily trigger a notification after a delay. Here we show one after 30 seconds:

let trigger = UNTimeIntervalNotificationTrigger(
  timeInterval: 30,
  repeats: false
)

Notifications are a huge part of making our apps useful. They allow us to notify the user both actively and passively of important information, providing the immediacy users have come to expect.

In iOS 10, Apple has overhauled how notifications work. Today we'll begin looking at these changes with the new User Notifications framework. Let's dive in. 🏊

It all starts with UNUserNotificationCenter, and more specifically:

UNUserNotificationCenter.current()

We'll use the current notification center to authorize, schedule, retrieve, and generally manage the notifications our app displays.

Before we can do anything, we'll need to ask for permission. We can pass in some options for the kinds of notification behaviors we'd like permission to use:

UNUserNotificationCenter.current()
  .requestAuthorization(
    options: [.badge, .sound, .alert, .carPlay]
  ) { granted, error in
    if granted {
      // yay!
    }
  }

For the next step, we'll return to our imaginary Spaceships application. Let's say we have a feature that alerts the user when an asteroid is detected in our path.

To show a notification to the user about the space rock, we'll need to create a new UNNotificationRequest and add it to the notification center.

We'll start by creating a UNMutableNotificationContent and configuring it for our needs:

let notification = UNMutableNotificationContent()

notification.categoryIdentifier = "asteroid-detected"

notification.title = "Asteroid Detected! πŸš€"
notification.body = "Collision imminent. Immediate action required!"

The categoryIdentifier here is simply a unique string we create. Later, we'll use this to identify "Asteroid Detected" notifications and differentiate them amongst from other kinds of notifications our app might display.

Next, we'll actually create the notification request:

let request = UNNotificationRequest(
  identifier: "asteroid-id-123",
  content: notification,
  trigger: nil
)

We haven't specified a trigger here, which tells the system we'd like this notification to be shown right away.

Finally, we'll add the request to the notification center:

UNUserNotificationCenter.current().add(request)

We'll wrap all this in a quick function and call it with a 2 second delay so we have time to press the home button and get to the home screen to see the notification appear.

import Dispatch

func applicationDidBecomeActive(_ application: UIApplication) {
  DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(2)) {
    showTestNotification()
  }
}

Success!

Push Notifications were a fantastic addition to iOS in iOS 3. They can also sometimes be tricky to test and debug. We can save ourselves a ton of time and hassle with OS X app called Knuff. Today we'll check it out and see what it can do. Let's get started.

Before we begin, we'll install the free Knuff iOS app to make our lives a little easier in a few minutes.

We'll launch it on our iOS device (on the same Wifi network as our Mac). Knuff will display a new Devices button for us to choose our device, then it will in our push token for us.

We can use the Knuff iOS tab to try a push right away, neat!

Next, we'll switch to the Custom tab where we'll pick our app's certificate identity:

Knuff helps us by finding all the identities in our Keychain and letting us pick. (We can create a new push certificate/identity for our app with one command using fastlane's pem tool).

That's it! We can now use Knuff to configure our payload, and send pushes. Knuff even lets us save our configurations into files that we can open later for testing different notifications and scenarios.

More info about Knuff can be found at git.io/knuff

Topics

#122: Notification Actions πŸ“’

Topics

Today we're going to look at how we can improve our app's notifications by adding actions to them. Notification Actions let our users perform a task directly inside the system's notification banner. Let's get started.

We'll be working in an imaginary Groceries app. The app currently notifies users when they're running low on something. We're going to make things even better by letting the user re-stock their pantry right from the notification banner.

First, we'll create a couple of UIMutableUserNotificationActions:

let reorderAction = UIMutableUserNotificationAction()
reorderAction.title = "Order More"
reorderAction.identifier = "com.groceries.running-low.reorder"
reorderAction.authenticationRequired = false
reorderAction.activationMode = .Background

let remindLaterAction = UIMutableUserNotificationAction()
remindLaterAction.title = "Remind Me Later"
remindLaterAction.identifier = "com.groceries.running-low.postpone"
remindLaterAction.authenticationRequired = false
remindLaterAction.activationMode = .Background

These action objects describe the buttons the user will see when they expand the notification banner after it appears.

Next, we'll attach these actions to a notification category, then pass that category in when we register for user notifications:

let category = UIMutableUserNotificationCategory()
category.identifier = "com.groceries.running-low"
category.setActions([reorderAction, remindLaterAction], 
  forContext: .Default)

application.registerUserNotificationSettings(
  UIUserNotificationSettings(forTypes: [.Sound, .Alert, .Badge],
    categories: [category]))

Finally, we'll implement a delegate function to handle our actions:

func application(
  application: UIApplication, 
  handleActionWithIdentifier identifier: String?,
  forRemoteNotification userInfo: [NSObject : AnyObject], 
  withResponseInfo responseInfo: [NSObject : AnyObject],
  completionHandler: () -> Void
) {
  // handle the action, based on its identifier
  completionHandler()
}

Topics

#43: Local Notifications πŸ“

Topics

Local Notifications are a way to display a system notification to a user of your app without needing to implement a push server. You simply schedule them using an NSDate and they are presented at that time. Let's take a look.

Register for Notifications

Since iOS 8, we've had two different things to register for: User Notifications and Remote notifications. Since we'll be β€œsending” Local Notifications, we only need to register for User Notifications.

In didFinishLaunchingWithOptions:

application.registerUserNotificationSettings(
  UIUserNotificationSettings(
    forTypes: [.Alert, .Sound, .Badge], 
    categories: nil
  )
)

This will prompt the user for permission to see notifications, except instead of coming from a backend server, they'll be generated locally on the device.

Schedule a Local Notification

After we get the didRegisterUserNotificationSettings callback, we're ready to schedule a notification. We use the UILocalNotification class to configure all the aspects of how the notification will look and behave:

func landSpaceship() {
  autopilotPerformAction(.LandShip)

  let landingTime = 30.0 // in seconds

  let n = UILocalNotification()

  n.alertTitle = "Spaceship Landed!"
  n.alertBody = "Good news everyone! Our ship has successfully landed."

  n.timeZone = NSCalendar.currentCalendar().timeZone
  n.fireDate = NSDate(timeIntervalSinceNow: landingTime)

  UIApplication.sharedApplication().scheduleLocalNotification(n)
}

CloudKit lives on the client. You write Swift or Objective-C code to interact with Apple's iCloud servers to save and retrieve data. But what if your app wants to support push notifications? Don’t worry CloudKit has you covered. Use CKSubscription to tell CloudKit you'd like to know when records matching a predicate are created, updated, or deleted. CloudKit will deliver a special push notification to your app denoting the change, which you can customize for your needs.

Let's take a look at setting it up: (don't forget to setup and register for push notifications!)

let publicDB = CKContainer.defaultContainer().publicCloudDatabase

let subscription = CKSubscription(
  recordType: "Spaceships",
  predicate: NSPredicate(format: "TRUEPREDICATE"),
  options: .FiresOnRecordCreation
)

let info = CKNotificationInfo()

info.alertBody = "New Spaceship Entered the Fleet!"
info.shouldBadge = true

subscription.notificationInfo = info

publicDB.saveSubscription(subscription) { record, error in }

Then when you receive the push notification use it's userInfo to create a CKNotification object and grab the ID of the new record:

let notification: CKNotification = CKNotification(
  fromRemoteNotificationDictionary: userInfo
)

if notification.notificationType == CKNotificationType.Query {
  let queryNotif = notification as! CKQueryNotification
  // do something interesting with queryNotif.recordID
}