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!
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:
RemoteDisplay on phone StartNoWatch Kit extensionSourceSent directly toApple WatchSent directly toApple WatchNoiPhoneunlocked andscreen onNotificationTypeYesiOS appLocalBackgroundYesYesYesApple Watch on wrist andunlockedDisplay on Apple WatchDisplay on phone NoNo
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.
Testing 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 Appscheme. You’ll see a collection of cute kitty cats that you can easily browse:
Testing Notifications With the Simulator¶
When you create a new Apple Watch app in Xcode, it no longer generates a notification scheme. Click Product ▸ Scheme ▸ New Scheme… and name the scheme Pawsome Notification:
Edit the newly created scheme and make three changes:
- Set the Executable to the watch app.
- Set the Watch Interface to Dynamic Notification.
- Set the Notification Payload to PushNotificationPayload.apns.
Using the newly created schema, build and run the app, then tap on the notification:
The information of what to display comes from the PushNotificationPayload file that you specified when creating the schema.
However, when watchOS delivers an actual notification, you have to specify what SwiftUI View to utilize. Take a look at LocalNotifications, 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, then add the following to the end of the body
:
WKNotificationScene(
controller: NotificationController.self,
category: LocalNotifications.categoryIdentifier
)
Calling WKNotificationScene
is how you tell watchOS what view to display for each category identified in your payload.
Build and run the app. Nothing looks different. Why isn’t the notification now using the defined NotificationController
?
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 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 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, 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 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.
- For the preview, 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 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": 19
Build and run again. You’ll see cat19 from the asset catalog.
Receiving 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. It’s also much more difficult to display custom notifications with SwiftUI. 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.
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 Swift file called ExtensionDelegate 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() {
Task {
let success = try await UNUserNotificationCenter
.current()
.requestAuthorization(options: [.badge, .sound, .alert])
guard success else { return }
// 4
await MainActor.run {
WKExtension.shared().registerForRemoteNotifications()
}
}
}
}
Here’s a code breakdown:
- You declare a class that implements
WKExtensionDelegate
. Since that protocol is based onNSObjectProtocol
you also need to derive fromNSObject
. - Just like in iOS, you grab the
deviceToken
whenever 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
WKExtension
singleton to register for push notifications, which callsdidRegisterForRemoteNotifications(withDeviceToken:)
if successful.
To tell watchOS it should use your ExtensionDelegate
, add the following two lines to the top of the struct
in PawsomeApp:
@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 Notificationsgroup 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.
Look at RemoteNotificationView, 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. 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!
Adding 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 select the Pawsome Watch App target.
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 correct target.
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, sometimes it doesn’t work.
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.yourcompany.Pawsome.watchkitapp and add Push Notification support.
Note
Replace yourcompany 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, or you can click on the current scheme and then click Edit Scheme….
Earlier, you created 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.
The starter project already includes an .apns file. In your own projects you’ll have to add a new one. 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"
}
Remember that watchOS has no way of associating a payload to a controller if you don’t link them. Go back into PawsomeApp and add the following statement:
WKNotificationScene(
controller: RemoteNotificationController.self,
category: RemoteNotificationController.categoryIdentifier
)
Build and run to take a look at how your remote notification would look on watchOS!
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
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.
Great work! Now you know how to add custom notification interfaces to your Watch apps. The rest is up to you! With the power of SwiftUI you are free to put any sort of view on the screen you can imagine, just like on iOS.
APNs Request¶
A key difference between iOS notifications and watchOS notifications is that you mustinclude 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.