6.
Notifications
Written by Scott Grosch
Local and remote notifications are a great way to inform your users about new or relevant information. There may be new content available, it might be their turn in the game or they may have just won the lottery. If your app has an accompanying iPhone app that supports notifications, by default, your Apple Watch will display the notification when appropriate. However, you can do better!
Notification is a massive topic that could fill a book all by itself. This chapter will focus solely on some of the differences you must be aware of when working with watchOS. You can learn everything you need to know about push notifications in our book, Push Notifications by Tutorials, which is available as part of the professional subscription.
Where did it go?
Apple tries to determine the best target device to receive a notification. If you only have an Apple Watch, it’ll go there. However, if you use a watch and another device, the destination depends not only on the type of notification but also on its source.
The diagram below will help you understand how Apple chooses which device should display the notification. As you can see, the notification type, whether it is Local, Remote or a Background notification, will define where it should go. For the first two options, local and background, it will prioritize the Apple Watch. Local Notifications on the other hand will prioritize depending on the source. Check the following image to see the different paths:
You’ll notice two locations in the diagram where it asks if Apple sent the notification directly to the watch. In watchOS 6 and later, the Apple Watch is a valid target for remote and background notifications. The Apple Watch extension receives a unique device token when registering for remote notifications, just like in iOS.
Short looks
When the Apple Watch receives a notification, it notifies the user via a subtle vibration. If the user views the notification by raising their wrist, the Apple Watch shows an abbreviated version called a short look. If the user views the notification for more than a split second, the Apple Watch will offer a more detailed version, or long look.
The short look notification is a quick summary for the user. Short looks show the app’s icon and name, as well as the optional notification title, in a predefined layout:
The optional notification title is a short blurb about the notification, such as “New Bill”, “Reminder” or “Score Alert”, and is added to the alert key’s value. This lets the user decide whether to stick around for the long look interface.
Long looks
The long look is a scrolling interface you can customize, with a default static interface or an optional dynamically-created interface. Unlike the short look interface, the long look offers significant customization.
The sash is the horizontal bar at the top. It’s translucent by default, but you can set it to any color and opacity value.
You can customize the content area by implementing a SwiftUI View, which you’ll learn about later.
While you can implement several UNNotificationAction items, remember that more than a few will require quite a bit of scrolling on the user’s part, leading to a poor user experience.
The system-provided Dismiss button is always present at the bottom of the interface. Tapping Dismiss hides the notification without informing the Apple Watch extension.
Now that you know about the short and long look notifications, it’s time to put the theory into practice.
Local notifications
Pawsome is for all cat lovers who procrastinate during the day by looking at cute cat pictures. The Pawsome app will make this easier by interrupting you throughout the day with cute cat pictures that are certain to trigger a smile… unless you’re a dog person!
Getting started
Open the Pawsome starter project in Xcode. Then build and run the Pawsome WatchKit App scheme. You’ll see a collection of cute kitty cats that you can easily browse:
Testing notifications with the simulator
Switch to the Pawsome WatchKit App (Notification) scheme and rerun the project. This time, instead of seeing those adorable cats, you’ll see a pretty boring notification:
It’s not the purrrtiest, but you’ll soon fix that.
Take a look at LocalNotifications.swift, and you’ll see the code that creates and schedules your local notifications. There’s nothing specific to watchOS, which is why I provided that file for you. At the top of the class, you’ll find categoryIdentifier. When a notification triggers, that’s the identifier you’ll use.
Open PawsomeApp.swift. You’ll see the following line, which Xcode adds if you say you want to include a notification when you create your project:
WKNotificationScene(
controller: NotificationController.self,
category: "myCategory"
)
Calling WKNotificationScene is how you tell the Apple Watch what view to display for each category identified in your payload. Replace the category with the one from LocalNotifications:
WKNotificationScene(
controller: NotificationController.self,
category: LocalNotifications.categoryIdentifier
)
Build and run the app. You’ll see this notification:
What happened? The NotificationView wasn’t updated, so why the change?
If you look at the Run step of the Pawsome WatchKit App (Notification) scheme, you’ll see the notification payload is set to a file in your project, called PushNotificationPayload.apns:
Take a look at PushNotificationPayload.apns. If you’ve worked with push notifications at all, this should look familiar to you. The category sent with the notification is set to myCategory. However, you updated PawsomeApp.swift to respond to a different category name. When the category sent to the app doesn’t match something you’ve registered for, it configures a default display.
Change myCategory to Pawsome, which is the value of LocalNotifications.categoryIdentifier.
Run the app again. This time you’ll see:
Where did the “More Cats!” button come from? PawsomeApp.swift creates an instance of LocalNotifications, which creates a default action button in its initializer.
Since the category in the JSON matches what you specified in WKNotificationScene(controller:category:), watchOS created an instance of NotificationController and used that to display the notification. Look closely, and you’ll notice the title and body are missing from the displayed view. Time to fix that!
Custom long look notification
Edit NotificationController.swift, and you’ll see body returns an instance of NotificationView. The controller is where you receive and parse the notification. The view is then where you use the data gathered by the controller.
Switch over to NotificationView.swift to make the notification appear the way you want. Replace the entire contents of the default file with:
import SwiftUI
struct NotificationView: View {
// 1
let message: String
let image: Image
// 2
var body: some View {
ScrollView {
Text(message)
.font(.headline)
image
.resizable()
.scaledToFit()
}
}
}
struct NotificationView_Previews: PreviewProvider {
static var previews: some View {
// 3
NotificationView(
message: "Awww",
image: Image("cat\(Int.random(in: 1...20))")
)
}
}
The code is pretty straight-forward:
- You need to provide a message and an image for the view to display.
- The body simply displays those two properties in a scrolling list.
- A random image is chosen from the asset catalog for the previews.
Now that you’ve created a view to display when a notification arrives, it’s time to use it. Head back to NotificationController.swift and replace the contents of the class with:
// 1
var image: Image!
var message: String!
// 2
override var body: NotificationView {
return NotificationView(message: message, image: image)
}
// 3
override func didReceive(_ notification: UNNotification) {
let content = notification.request.content
message = content.body
let num = Int.random(in: 1...20)
image = Image("cat\(num)")
}
Like all good code, the controller is short and sweet. It only has a few steps:
- You store the title and image so you can send them to the view.
- Then you call the initializer for the view you’re going to display, passing in the appropriate parameters.
- You pull out the details from the payload body, which you then store in the class’ properties.
Build and run again, trying to limit the immense feelings of joy caused by the display:
You probably want to display a specific cat, not a random one. Replace the last two lines of didReceive(_:) with:
let validRange = 1...20
if
let imageNumber = content.userInfo["imageNumber"] as? Int,
validRange ~= imageNumber {
image = Image("cat\(imageNumber)")
} else {
let num = Int.random(in: validRange)
image = Image("cat\(num)")
}
The asset catalog provided with the starter project has images numbered from one to twenty. If the payload includes an image number in that range, you display the specified cat photo. If not, you provide a random image.
Note: Since the view will always be generated, you need to ensure that valid data is always available, even if it means presenting a default set of data.
Add the following line right before the final closing } character in PushNotificationPayload.apns:
, "imageNumber": 5
Build and run again. You’ll see cat5 from the asset catalog.
Remote push notifications
Most apps use push notifications, not local notifications. You’re probably wondering why I spent all that time on something that’s used less frequently. Well, the answer is that Apple made push notifications much easier on watchOS than they are on iOS.
In iOS, you have to create an extension to modify the incoming payload and yet another extension if you want a custom interface. Worse yet, you can’t perform the custom interface with SwiftUI as of iOS 15. In watchOS, push notifications work exactly like local notifications!
Everything you learned about using WKUserNotificationHostingController to parse the payload and return a custom SwiftUI View works the same when you’re developing push notifications.
Token creation
This chapter assumes you already created your push notification token, a file you download from the Apple developer portal that ends with a p8 extension. If you need help generating your token, please see our Push Notifications by Tutorials book.
Don’t worry if you can’t create a token file. For example, you might not have permission to create a token in your team’s portal. This chapter will use the simulator and a JSON file to simulate receiving a remote push notification.
Create a WKExtensionDelegate
In iOS, you register for push notifications using AppDelegate. That class doesn’t exist on watchOS. Instead, you use WKExtensionDelegate. Create a new file called ExtensionDelegate.swift and paste:
import WatchKit
import UserNotifications
// 1
final class ExtensionDelegate: NSObject, WKExtensionDelegate {
// 2
func didRegisterForRemoteNotifications(withDeviceToken deviceToken: Data) {
print(deviceToken.reduce("") { $0 + String(format: "%02x", $1) })
}
// 3
func applicationDidFinishLaunching() {
async {
do {
let success = try await UNUserNotificationCenter
.current()
.requestAuthorization(options: [.badge, .sound, .alert])
guard success else { return }
// 4
await MainActor.run {
WKExtension.shared().registerForRemoteNotifications()
}
} catch {
print(error.localizedDescription)
}
}
}
}
Here’s a code breakdown:
- You declare a class that implements
WKExtensionDelegate. Since that protocol is based onNSObjectProtocolyou also need to derive fromNSObject. - Just like in iOS, you grab the
deviceTokenwhenever registration happens. A production app would, of course, store and use the token, not just print it. - Surprisingly not named
extensionDidFinishLaunching. You do the standard dance in this method to request permission to use push notifications. - If permissions are granted, you use the
WKExtensionsingleton to register for push notifications, which calldidRegisterForRemoteNotifications(withDeviceToken:)if successful.
To tell watchOS it should use your ExtensionDelegate, add the following two lines to the top of the struct in PawsomeApp.swift:
@WKExtensionDelegateAdaptor(ExtensionDelegate.self)
private var extensionDelegate
The MVC of push notifications
Instead of making you copy and paste a ton of code, I’ve provided a Remote Notifications group in the starter project, which contains the relevant files for a push notification.
Generally, you’ll want to use some type of model to represent the data that will pass between your WKUserNotificationHostingController and View. While you could use individual properties, as in the preceding examples for local notifications, it’s better to use a real model, such as the one provided in RemoteNotificationModel.swift.
Look at RemoteNotificationView.swift, and you’ll see it’s just a simple setup that shows a small number of details by default. If you tap the toggle, it displays more details. Remember, unlike the iPhone, the Apple Watch has limited display space. You’ll need to think differently about how you present notification data to the user.
Next, open RemoteNotificationController.swift. Even though you’re working with a remote push notification, you’ll see everything works the same as when you implemented local notifications. Pay special attention to the guard statement.
In an ideal world, the payload provided to your app would always be 100% perfect. However, we don’t live in an ideal world, so it’s important to always validate the input. If anything goes wrong, you still have to provide a model for the notification to display. Don’t let your app crash because of bad data!
Add the capability
Xcode will perform magic if you add the Push Notifications capability. In the Project navigator, Command‑1, select the project name, Pawsome. Then on the right, in the project editor, select the extension target. Make sure you choose the extension, not the app. Generally, that’ll be the last target listed.
On the Signing & Capabilities tab, add the Push Notifications capability. If you try to add a capability and nothing seems to show, that generally means you haven’t chosen the extension.
Note: Xcode does not generate a valid identifier for watchOS push notifications!
Years ago, Apple finally automated almost everything around certificate and identifier generation. Unfortunately, for watchOS push notifications, it broke things.
When sending a push notification directly to the Apple Watch, you need to use the app’s bundle identifier, not the extension’s bundle identifier. In the case of Pawsome, that means you send your push to com.raywenderlich.Pawsome.watchkitapp and not com.raywenderlich.Pawsome.watchkitapp.watchkitextension. Unfortunately, because you have to add the capability to the extension, the generated profile thinks you’re using the latter identifier, not the former.
The solution, while annoying to have to deal with, is quite simple. Go into your team’s Certificates, Identifiers & Profiles page on the Apple Developer Portal and manually create an identifier for your watchOS app. Use the app identifier com.raywenderlich.Pawsome.watchkitapp and add Push Notification support.
Note: Replace raywenderlich in the app identifier with your team’s name.
Add a scheme
Edit the current scheme via your preferred method. I like to press the Command+< keyboard shortcut.
The default project Xcode created includes a notification scheme you used to test the local notifications. You’ll need another scheme to test the remote notification because the payload is a different file. You could, of course, just change the file on the existing scheme if you wanted to.
Note: If your app supports more than one notification, you can add multiple APNS files and multiple schemes to make it easy to test each one.
Follow these steps to configure a new scheme:
- With the notification scheme still chosen, press Duplicate Scheme at the bottom of the dialog window.
- Choose a name for the new scheme, such as Push Notification.
- Click Run on the left side of the dialog window.
- Select RemotePush.apns as the notification payload to use.
You’ll find RemotePush.apns in Remote Notifications. It contains a simple example remote push notification payload:
{
"aps": {
"alert": {
"body": "Lorem ipsum dolor sit amet, consectetur...",
"title": "Lorem Ipsum",
},
"category": "lorem"
},
"date": "2021-04-01T12:00:00Z"
}
Build and run. You probably expected to see the following short and long look notifications:
Take a moment to try and figure out why you see something different.
Remember that watchOS has no way of associating a payload to a controller if you don’t link them. Go back into PawsomeApp.swift and add the following statement:
WKNotificationScene(
controller: RemoteNotificationController.self,
category: RemoteNotificationController.categoryIdentifier
)
Build and run again. You’ll now see the proper notification screen.
Interactive notifications
Tap Show details. Did something unexpected happen? An average user would expect to see details. Instead, you were taken into the app and shown a cat picture. Surprisingly, that’s by design.
By default, push notifications are not interactive. As far as the Apple Watch knows, anything that’s not one of the action buttons is just an image on the screen.
Adding an action button wouldn’t make sense to show more details. Instead, add the following line to RemoteNotificationController:
override class var isInteractive: Bool { true }
Build and run again. This time, when you tap the toggle, the details appear and disappear as you’d expect them to:
The isInteractive type property of WKUserNotificationHostingController<T> specifies whether the notification should accept user input. The default value is false, meaning you can only interact with buttons. By changing the value to true, you tell watchOS the notification should accept user input.
You solved one problem but might have introduced another. Tapping no longer opens the app. If the user taps the app icon or anywhere on the sash, the app will still open.
Styling
If the default colors don’t work well with your app’s theme, you can override a few properties to perform minimal customization.
Edit RemoteNotificationController.swift and add the following lines at the end of the class:
override class var sashColor: Color? {
Color(red: 0, green: 156 / 255, blue: 83 / 255)
}
override class var titleColor: Color? { Color.purple }
override class var subtitleColor: Color? { Color.orange }
override class var wantsSashBlur: Bool { true }
Build and run again. You’ll notice the colors have changed to something hideous:
The only short look customization you make is changing the subtitle to orange, as specified by subtitleColor.
For the long look, you made three changes. You set the sash color to the official Razeware green via sashColor and modified the title text inside the sash to purple via titleColor.
wantsSashBlur, which defaults to false, determines whether the sash includes a blur over the background.
Great work! Now you know how to add custom notification interfaces to your Watch apps.
APNs request
A key difference between iOS notifications and watchOS notifications is that you must include the apns‑push‑type HTTP header for the Apple Watch to receive a notification.
Apple’s Sending Notification Requests to APNs documents the HTTP headers that you might need to send as part of your POST to APNs.
Key points
- How Local & Remote Notifications work with Apple Watch.
- Short & Long looks and how to customize them.
- Testing Push Notifications on Apple Watch
- Ensure your server includes the apns‑push‑type HTTP header.
Where to go from here?
In this chapter, you tested Watch notifications, learned about short look and long look interfaces and how they differ. Most impressively, you built a custom, dynamically updating, long look local notification for the Apple Watch.
Now you know the basics of showing custom notifications on watchOS, but there’s a lot more you can do from here, including handling actions selected by users from your notifications. Please see Apple’s User Notifications documentation as well as our Push Notifications by Tutorials book, which is available as part of the professional subscription.
For more details on creating schemes and JSON payloads, as well as testing directly on the watch, please see Testing Custom Notifications in Apple’s developer documentation.