- Published on
Scheduled Notifications
- Authors
- Name
- Misiel Rodriguez
- @misiel
My goal with making the Expired app was to learn how notifications work on iOS. That's been my MO with the past couple side projects I've worked on - come across a user experience I'm curious about (share sheet, notifications, etc) and then create a mini-app centered around that UX to learn how it works. So the idea for Expired was to create an app that reminds me when food is about to expire from a user inputted date as well as a second user-selected reminder but with pre-determined dates (3 days before, 1 week before, 2 weeks before). Let's dive into how notifications work!
Purpose of a Notification
There's 2 types of notifications on iOS - local and push. Push notifications are sent from a remote server (an example for this is Firebase) to targeted devices. It could be devices in a certain region or with a certain OS to tell users to update their devices. Local notifications are registered on the iOS device. A famous example is the Reminders app that comes with iOS by default. Expired is using local notifications for this simple example but I will for sure be making a client-server based app with Firebase soon!
One thing that caught my eye when I was working on notifications is that unless specified, by default notifications will not trigger while the app is active in the foreground. iOS wants notifications to be used intentfully - meaning they're a sort of invasive alert used to grab the user's attention and notify them about an important update. Some games use it to notify the user that their daily objective is up for grabs. A health app might use it to notify the user it's time to take their medication. Expired is using it to tell the user to throw their gross food out!
How It Works
iOS has 1 object that controls the behavior of notifications in your app, UNUserNotificationCenter. Through this object, you can create the notification, delete it, set its behavior, request authorization, schedule the delivery etc. So how do you use UNUserNotificationCenter to do this?
- Request Authorization.
Remember how I called notifications invasive earlier? Well they are! As a user, you don't want just any app to bombard you with an alert at any time of the day and while you're doing anything on your phone. So the first thing is to request the user for authorization for your app to be able to send them notifications.
In Expired I do this in the viewDidLoad()
of the first screen HomeViewController
. It might be better practice to add this to AppDelegate - didFinishLaunchingWithOptions
as this is probably the very first entry point into the app.
private func requestNotificationUserPermissions() {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound], completionHandler: {success, error in
if success {
print("authorization granted")
}
else if let error = error {
print("error occurred: \(error)")
}
})
}
Let's break this down.
- The same way we've been using
HomeViewModel.shared
as a singleton instance throughout our apps,UNUserNotificationCenter.current()
provides a singleton instance as well. This is a good coding practice to manage resources, promote shared data and coordinate between your objects. requestAuthorization(options: ,completionHandler: )
is a method that takes in the types of authroizations we need from our user and also a completionHandler that returns a success and error to alert you if the user granted permission or not. For our notification, we are requesting the user for alert, badge, and sound privileges.
- Creating The Notification
- What does the notification look like?
private func addFoodItemNotification(with foodItem: FoodItem) {
// 1) create noti content
let content = UNMutableNotificationContent()
content.title = "🤢 Throw It Out!"
content.sound = .default
content.body = "Your \(foodItem.foodName) has expired."
...
}
In HomeViewModel
whenever a user adds a new food item, we also call this addFoodItemNotification
method with the new food item. There are several components in this method:
- What does the notification look like? Notifications take this
UNMutableNotificationContent()
object that is modifiable with a title, sound and body. Here's what this notification would look like:
- When will we send the notification?
// 2) create the trigger with date
let expDate = foodItem.expDate
let expTrigger = UNCalendarNotificationTrigger(dateMatching: Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: expDate), repeats: false)
When notifications are sent is determined by triggers. There are several different types but because our notification is date dependent, we are using a UNCalendarNotificationTrigger
. Due to the structure of FoodItem
(yay to good coding practices), we already have a variable for our Date that we need when we want to send this notification. The UNCalendarNotificationTrigger is initialized with a dateMatching
parameter of type DateComponent
which allows you to get granular with the timeliness of your notification by specifying if you are using the year, month, day... all the way down to the second and then it has a from
parameter as well which is where it's getting the date from.
- Register the notification with UNUserNotificationCenter
// 3) create a request with content and trigger and request to NotiCenter
let request = UNNotificationRequest(identifier: uuidString, content: content, trigger: expTrigger)
UNUserNotificationCenter.current().add(request)
Now that we have our content and trigger for our notification, the next part is to simply register the notification. First we have to create a request object for our notification. UNNotificationRequest
has 3 parameters: content
, trigger
and an optional identifier
. Then we add the request to the UNUserNotificationCenter with .add()
You typically don't need an identifier for your notification, but what if you want to remove a scheduled notification... say when you delete the food item from your saved list?
- Generating a unique identifier for your notification
// generating uniqueID to remove noti later
let uuidString = UUID(uuidString: foodItem.foodName)?.uuidString ?? "notification_id"
foodItem.setUniqueNotiId(uuidString: uuidString)
Swift has a handy UUID
class exactly for this scenario. Given a string, it will generate a unique string from it. And it's not like the same inputted string will always give you the same output either, it is always a randomly generated output when computated. So with this uniquely generated ID for each registered food item, I can now call this method when the user deletes the food item for whatever reason.
func removeFoodItem(removedFoodItem: FoodItem, updatedList: [FoodItem]) {
// ...
self.removeFoodItemNotification(foodId: removedFoodItem.uuidString)
// ...
}
private func removeFoodItemNotification(foodId: String) {
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [foodId])
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [foodId])
}
removePendingNotificationRequests
removes the scheduled notification from appearing if it hasn't been seen yet. removeDeliveredNotifications
does the same thing but if the notification has been delivered already. I think this is more useful for a notification that repeats.
Conclusion
And that's all it takes to make a notification in iOS!
- Request User permission
- Create the content for the notification
- Determine what trigger you need for your noti (when should a user get this)
- Register it to the UNUserNotificationCenter
Thanks for reading!