20.
Local Notifications
Written by Matthijs Hollemans & Fahim Farook
I hope you’re still with me! We have discussed view controllers, navigation controllers, storyboards, segues, table views and cells, and the data model in great detail. These are all essential topics to master if you want to build iOS apps because almost every app uses these building blocks.
In this chapter you’re going to expand the app to add a new feature: local notifications, using the iOS User Notifications framework. A local notification allows the app to schedule a reminder to the user that will be displayed even when the app is not running.
You will add a “due date” field to the ChecklistItem object and then remind the user about this deadline with a local notification.
If this sounds like fun, then keep reading. :-)
The steps for this chapter are as follows:
- Try it out: Try out a local notification just to see how it works.
- Set a due date: Allow the user to pick a due date for to-do items.
- Due date UI: Create a date picker control.
- Schedule local notifications: Schedule local notifications for the to-do items, and update them when the user changes the due date.
Try it out
Before you think about how to integrate local notifications with Checklists, let’s just schedule a local notification and see what happens.
By the way, local notifications are different from push notifications – also known as remote notifications. Push notifications allow your app to receive messages about external events, such as your favorite team winning the World Series and have to be sent to your device from a remote server.
Local notifications are more similar to an alarm clock: you set a specific time and then it “beeps”. Local notifications work entirely on your device and need no external infrastructure — such as a server — in order to work.
Get permission to display local notifications
An app is only allowed to show local notifications after it has asked the user for permission. If the user denies permission, then any local notifications for your app simply won’t appear. You only need to ask for permission once, so let’s do that first.
➤ Open AppDelegate.swift and add a new import to the top of the file:
import UserNotifications
This tells the compiler that we’re going to use the User Notifications framework.
➤ Add the following to application(_:didFinishLaunchingWithOptions:), just before the return true line:
// Notification authorization
let center = UNUserNotificationCenter.current()
center.requestAuthorization(options: [.alert, .sound]) {granted, error in
if granted {
print("We have permission")
} else {
print("Permission denied")
}
}
The application(_:didFinishLaunchingWithOptions:) method of the app delegate is called when the app starts up. It is the entry point for the app, the first place in the code where you can do something after the app launches.
Because you’re just playing with these local notifications now, this is a good place to ask for permission.
You tell iOS that the app wishes to send notifications of type “alert” with a sound effect. Later you’ll move this code to a more appropriate location.
Things that start with a dot
Throughout the app you’ve seen things like .none, .checkmark, and .subtitle — and now .alert and .sound. These are enumeration symbols.
An enumeration, or enum for short, is a data type that consists of a list of possible symbols and their values.
For example, the UNAuthorizationOptions enum contains the following symbols (and a few others besides):
.badge
.sound
.alert
.carPlay
You can combine these enums in an array to define what sort of notifications the app will show to the user. Here you’ve chosen the combination of an alert and a sound effect by writing [.alert, .sound].
It’s easy to spot when an enum is being used because of the dot in front of the symbol name. This is actually shorthand notation; you could also have written it like this:
center.requestAuthorization(options: [UNAuthorizationOptions.alert, UNAuthorizationOptions.sound]) {
. . .
Fortunately, Swift is smart enough to realize that .alert and .sound are from the enum UNAuthorizationOptions, so you can save yourself some keystrokes.
➤ Run the app. You should immediately get a popup asking for permission:
Tap Allow. The next time you run the app you won’t be asked again; iOS remembers your choice.
If you tapped Don’t Allow — naughty! — then you can always reset the Simulator to get the permissions dialog again. You can also change the notification options via the Settings app.
Show a test local notification
➤ Stop the app and add the following code to the end of didFinishLaunchingWithOptions before the return:
let content = UNMutableNotificationContent()
content.title = "Hello!"
content.body = "I am a local notification"
content.sound = UNNotificationSound.default
let trigger = UNTimeIntervalNotificationTrigger(
timeInterval: 10,
repeats: false)
let request = UNNotificationRequest(
identifier: "MyNotification",
content: content,
trigger: trigger)
center.add(request)
This creates a new local notification. Because you wrote timeInterval: 10, it will fire exactly 10 seconds after the app has started.
The UNMutableNotificationContent describes what the local notification will say. Here, you set an alert message to be shown when the notification fires. You also set a sound.
Finally, you add the notification to the UNUserNotificationCenter. This object is responsible for keeping track of all the local notifications and displaying them at the scheduled time.
➤ Run the app. Immediately after it has started, exit to the home screen.
Wait 10 seconds… I know, it seems like an eternity! After an agonizing 10 seconds a message should pop up:
➤ Tap the notification and it should take you back to the app.
And that’s a local notification. Pretty cool, huh?
Why did I want you to exit to the home screen? iOS will only show a notification alert if the app is not currently active.
➤ Stop the app and run it again. This time don’t press Home and just wait.
Well, don’t wait too long — nothing will happen. The local notification does get fired, but it is not shown to the user. To handle this situation, we must listen somehow to interesting events that concern these notifications. How? Through a delegate, of course!
Handle local notification events
➤ Add the notification delegate to AppDelegate’s class declaration:
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
This makes AppDelegate the delegate for the UNUserNotificationCenter.
➤ Also add the following method to AppDelegate.swift:
// MARK: - User Notification Delegates
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
print("Received local notification \(notification)")
}
This method will be invoked when the local notification is posted and the app is still running. You won’t do anything here except log a message to the debug pane.
When your app is active and in the foreground, it is supposed to handle any fired notifications on its own. Depending on the type of app, it may make sense to react to the notification, for example to show a message to the user or to refresh the screen.
➤ Finally, tell the UNUserNotificationCenter that AppDelegate is now its delegate. You do this in application(_:didFinishLaunchingWithOptions:). Add this after you ask for permission — perhaps when permission is granted?:
center.delegate = self
➤ Run the app again and just wait – don’t press Home.
After 10 seconds you should see a message in the Xcode Console. It displays something like this:
Received local notification <UNNotification: 0x600001336790; source: com.razeware.Checklists date: 2020-08-09 19:29:39 +0000, request: <UNNotificationRequest: ...
identifier: MyNotification, content: <UNNotificationContent: ...
title: <redacted>, subtitle: (null), body: <redacted>,
. . .
Note: In case you’re wondering why some values are shown as
<redacted>, that’s due to privacy constraints in iOS which stops possibly sensitive information, such as the contents of a notification message, being captured/logged by an application.
All right, now you know that it works, you should remove the test code from AppDelegate.swift because you don’t really want to schedule a new notification every time the user starts the app.
➤ Remove the the local notification code from didFinishLaunchingWithOptions, but keep these lines:
let center = UNUserNotificationCenter.current()
center.delegate = self
You can also keep the userNotificationCenter(_:willPresent:withCompletionHandler:) method, as it will come in handy when debugging the local notifications.
Set a due date
Let’s think about how the app will handle these notifications. Each ChecklistItem will get a due date property (a Date object, which stores a date and time) and a Bool that says whether the user wants to be reminded of this item or not.
Users might not want to be reminded of everything, so you shouldn’t schedule local notifications unless the user asks for it. Such a Bool variable is often called a flag. Let’s name it shouldRemind.
When do you schedule a notification?
First, let’s figure out how and when to schedule the notifications. I can think of the following situations:
-
When the user adds a new
ChecklistItemobject that has theshouldRemindflag set, you must schedule a new notification. -
When the user changes the due date on an existing
ChecklistItem, the old notification, if there is one, should be cancelled and a new one scheduled in its place ifshouldRemindis still set. -
When the user toggles the
shouldRemindflag from on to off, the existing notification should be cancelled. The other way around, from off to on, should schedule a new notification. -
When the user deletes a
ChecklistItem, its notification, if it had one, should be cancelled. -
When the user deletes an entire
Checklist, all the notifications for those items, if there are any, should be cancelled.
This makes it obvious that you don’t need just a way to schedule new notifications, but also a way to cancel them.
You should probably also check that you don’t create notifications for to-do items whose due dates are in the past. I’m sure iOS is smart enough to ignore those notifications, but let’s be good citizens anyway.
Associate to-do items with notifications
We need some way to associate ChecklistItem objects with their local notifications. This requires some changes to our data model.
When you schedule a local notification, you create a UNNotificationRequest object. It is tempting to put the UNNotificationRequest object as an instance variable in ChecklistItem, so you always know what it is. However, this is not the correct approach.
Instead, you’ll use an identifier. When you create a local notification, you need to give it an identifier, which is just a String. It doesn’t really matter what is in this string, as long as it is unique for each notification.
To cancel a notification at a later point, you don’t use the UNNotificationRequest object but the identifier you gave it. The right approach is to store this identifier in the ChecklistItem object.
Even though the identifier for the local notification is a String, you’ll give give each ChecklistItem an identifier that is simply a number. You’ll also save this item ID in the Checklists.plist file. When it’s time to schedule or cancel a local notification, you’ll turn that number into a string. Then, you can easily find the notification when you have the ChecklistItem object, or the ChecklistItem object when you have the notification object.
Assigning numeric IDs to objects is a common approach when creating data models — it is very similar to giving records in a relational database a numeric primary key, if you’re familiar with that sort of thing.
➤ Add these properties to ChecklistItem.swift:
var dueDate = Date()
var shouldRemind = false
var itemID = -1
Note that you called the last variable itemID and not simply “id”. The reason is that id is a special keyword in Objective-C, and this could cause trouble if you ever wanted to mix your Swift code with Objective-C code.
In the above code, you have initialized itemID with -1. However, what you really want is to have itemID set to a unique integer value when a new ChecklistItem instance is created. You can do this by adding a custom initializer to ChecklistItem.
But before you do that, you need to add a new method to DataModel to generate a unique item ID.
➤ Hop on over to DataModel.swift and add a new method:
class func nextChecklistItemID() -> Int {
let userDefaults = UserDefaults.standard
let itemID = userDefaults.integer(forKey: "ChecklistItemID")
userDefaults.set(itemID + 1, forKey: "ChecklistItemID")
return itemID
}
You’re using your old friend UserDefaults again.
This method gets the current “ChecklistItemID” value from UserDefaults, adds 1 to it, and writes it back to UserDefaults. It returns the previous value to the caller.
You could add a default value for “ChecklistItemID” to the registerDefaults() method so as to customize the start value for the item ID, but you really don’t have to in this case :] Remember that if there is no existing value for “ChecklistItemID”, you’d get 0 back from a call to UserDefaults – if you didn’t provide a default value via registerDefaults(). That is good enough for your use since your IDs would then start at 0 and count up.
The first time nextChecklistItemID() is called, it will return the ID 0. The second time it is called, it will return the ID 1, the third time it will return the ID 2, and so on. The number is incremented by one each time. You can call this method a few billion times before you run out of unique IDs.
Class methods vs. instance methods
If you are wondering why you wrote:
class func nextChecklistItemID()
And not just:
func nextChecklistItemID()
Then I’m glad you’re paying attention!
The inclusion of the class keyword in the method declaration indicates a class method — this kind of method applies to the class as a whole. So far you’ve been using instance methods. They just have the word func — without class — and work only on a specific instance of that class.
We haven’t discussed the difference between classes and instances before, and you’ll get into that in more detail later in the book. For now, just remember that a method starting with class func allows you to call methods on an object even when you don’t have a reference to that object.
Adding the class keyword means that you can call this method without having a reference to an instance of the DataModel object.
With a class method, you do:
itemID = DataModel.nextChecklistItemID()
Instead of:
itemID = dataModel.nextChecklistItemID()
This is because ChecklistItem objects do not have a dataModel property with a reference to a DataModel object. You could certainly pass them such a reference, but I decided that using a class method was easier.
I had to make a trade-off: is it worth giving each ChecklistItem object a reference to the DataModel object, or can I get away with a simple class method? To keep things simple, I chose the latter. It’s certainly possible that, if you were to develop this app further, it would make more sense to give ChecklistItem a dataModel property instead. But that would be up to you :]
➤ Now, switch back to ChecklistItem.swift and add an init() method to set up the unique ID:
override init() {
super.init()
itemID = DataModel.nextChecklistItemID()
}
This asks the DataModel object for a new item ID whenever the app creates a new ChecklistItem object and replaces the initial value of -1 with that unique ID.
Display the new IDs
For a quick test to see if assigning these IDs works, you can add them to the text that’s shown in the ChecklistItem cell label — this is just a temporary thing for testing purposes, as users couldn’t care less about the internal identifier of these objects.
➤ In ChecklistViewController.swift, change the configureText(for:with:) method to:
func configureText(
for cell: UITableViewCell,
with item: ChecklistItem
) {
let label = cell.viewWithTag(1000) as! UILabel
//label.text = item.text
label.text = "\(item.itemID): \(item.text)"
}
I have commented out the original line because you’ll want to reuse it later. The new one uses \( … ) to add the to-do item’s itemID property to the text.
Before you run the app, do note that you have changed the format of the ChecklistItem and thus, by extension, the Checklists.plist file. So your existing data will not display when you run the app.
➤ Run the app and add some checklist items. Each new item should get a unique identifier. Exit to the home screen to make sure everything is saved properly, and stop the app.
➤ Run the app again and add some new items; the IDs for these new items should start counting at where the numbering previously left off.
OK, that takes care of the IDs. Now lets add the “due date” and “should remind” fields to the Add/Edit Item screen.
Keep configureText(for:with:) the way it is for the time being; that will come in handy with testing the notifications.
Due date UI
You will add settings for the two new fields to the Add/Edit Item screen and make it look like this:
The due date field will require some sort of date picker control. iOS comes with a cool date picker view that you’ll add to the table view.
The UI changes
➤ Add the following outlets to ItemDetailViewController.swift:
@IBOutlet var shouldRemindSwitch: UISwitch!
@IBOutlet var datePicker: UIDatePicker!
➤ Open the storyboard and select the Table View in the Add Item scene.
➤ Add a new section to the table. The easiest way to do this is to increment the Sections value in the Attributes inspector. This duplicates the existing section and cell.
➤ Remove the Text Field from the new cell. Select the new section in the Document Outline and then increment its Rows value to 2 in the Attributes inspector.
You will now design the new cells to look as follows:
➤ Add a Label to the first cell and set its text to Remind Me. The font should be System, size 17.
➤ Also drag a Switch control into the cell. Hook it up to the shouldRemindSwitch outlet on the view controller. In the Attributes inspector, set its State to Off so it is no longer green.
➤ Pin the Switch to the top, right, and bottom edges of the table view cell. This makes sure the control will be visible regardless of the width of the device’s screen.
➤ Center the Label (vertically) with respect to the Switch, and then pin its left and right edges – make sure that the right edge is pinned with 8 points instead of the existing value.
➤ The third cell has two controls: a Due Date label on the left and a date picker control on the right. First, drag a label on to the left and change its title to Due Date. The font for the label should already be set to System, size 17
➤ Now drag a Date Picker control on to the cell. Yes, it will be bigger than the cell. Don’t worry, we’ll fix that in a moment.
➤ Make sure that the Style is set to Automatic in the Attributes Inspector for the Date Picker and the Mode is set to Date and Time.
➤ Select the Date Picker and pin it to the top, right, and bottom edges of the table view cell with a value of 4 for the top and bottom and 16 for the right. Pin the width to 230.
➤ Hook the Date Picker up to the datePicker outlet on the view controller.
➤ Select the Due Date label, Center Vertically with respect to the Date Picker, and then pin its left and right edges where the right edge should be 8.
You may need to adjust the Auto Layout constraints for the Remind Me label and the switch to align them nicely with the labels from the “due date” cell.
Tip: select the “Due Date” label and Date Picker and look in the Size inspector to see what their positions are – should be 16 points from the edges.
Note: Pre-iOS 14, the Date Picker control was 216 points tall and would not have fit in the table view cell. You had to do a lot more set up (and a lot more code) to set up a date picker to be usable in the app. But all that is no longer needed as of iOS 14.
Display the due date
Let’s write the code for displaying the due date.
➤ Change viewDidLoad() as follows:
override func viewDidLoad() {
. . .
if let item = itemToEdit {
. . .
shouldRemindSwitch.isOn = item.shouldRemind // add this
datePicker.date = item.dueDate // add this
}
}
If there is an existing ChecklistItem object, you set the switch control to on or off, depending on the value of the object’s shouldRemind property. If the user is adding a new item, the switch is initially off — remember, you already did that in the storyboard.
You also get the due date from the ChecklistItem and set the displayed date in the date picker.
But what about a new ChecklistItem item?
If you check the date picker on the storyboard, you’ll notice that in the Attributes Inspector, the Date value is set to “Current Date”. This means that, for a new ChecklistItem item, the due date will be right now. That sounds reasonable, but by the time the user has filled in the rest of the fields and pressed Done, that due date will be in the past.
But you do have to have a value there. You could set the Date value via code to be an alternative value such as this time tomorrow, or ten minutes from now. I’ll leave that as an exercise for you to try.
Update edited values
➤ The last thing to change in this file is the done() action. Replace the current code with:
@IBAction func done() {
if let item = itemToEdit {
item.text = textField.text!
item.shouldRemind = shouldRemindSwitch.isOn // add this
item.dueDate = datePicker.date // add this
delegate?.itemDetailViewController(
self,
didFinishEditing: item)
} else {
let item = ChecklistItem()
item.text = textField.text!
item.checked = false
item.shouldRemind = shouldRemindSwitch.isOn // add this
item.dueDate = datePicker.date // add this
delegate?.itemDetailViewController(
self,
didFinishAdding: item)
}
}
Here you put the value of the switch control and the date picker back into the ChecklistItem object when the user presses the Done button.
➤ Run the app and change the position of the switch control.
➤ Now tap on the date picker – it should show you an on-screen calendar where you can pick the date you want. Nifty!
You can tap on the month and year on the top left to change the month and the year — and tap on the top left control again to get back to the calendar. You can also tap on the two arrows on the top right to go back/forward month-by-month. Of course, you can enter the date directly into the date field at the bottom.
Once you are done, tap outside the calendar to dismiss it. Simple.
The app will remember your changes when you terminate it – be sure to exit to the home screen first though.
As I mentioned previously, this used to take a lot more code to get to this point before iOS 14. But now, it’s as simple as dropping a couple of controls on to the storyboard and updating their values!
Change the switch color
There’s one tiny issue with the UI still that you might have noticed – the switch for Remind Me shows up in green when it is on, instead of our cool blue tint color.
This is easy enough to fix.
➤ Open the storyboard, select the switch in the Add Item scene and change On Tint in the Attributes Inspector to the same blue that you set the Global Tint to. Generally, that color should be in your last used color list and you won’t even have to enter the color values :]
➤ Run the app again and make sure that the switch now displays the correct color.
Schedule local notifications
One of the principles of object-oriented programming is that objects should do as much as possible themselves. Therefore, it makes sense that the ChecklistItem object should schedule its own notifications.
Schedule notifications
➤ Add the following method to ChecklistItem.swift:
func scheduleNotification() {
if shouldRemind && dueDate > Date() {
print("We should schedule a notification!")
}
}
This compares the due date on the item with the current date — you can always get the current date (and time) by making a new Date object.
The statement dueDate > Date() compares the two dates and returns true if dueDate is in the future and false if it is in the past.
If the due date is in the past, the print() will not be performed.
Note the use of the “and” (&&) operator. You only print the text when the Remind Me switch is set to “on” and the due date is in the future.
You will call this method when the user presses the Done button after adding or editing a to-do item.
➤ In the done() action in ItemDetailViewController.swift, add the following line just before the call to didFinishEditing and also before didFinishaAdding:
item.scheduleNotification()
➤ Run the app and try it out. Add a new item, set the switch to ON but don’t change the due date. Press Done.
There should be no message in the Console because the due date has already passed — it is several seconds in the past by the time you press Done.
➤ Add another item, set the switch to ON, and choose a due date in the future.
When you press Done now, the text “We should schedule a notification!” should appear in the Console.
Now that you’ve verified the method is called in the proper place, let’s actually schedule a new local notification object for the following three scenarios: adding a to-do item, editing a to-to item, deleting a to-do item.
Add a to-do item
➤ In ChecklistItem.swift, change scheduleNotification() to:
func scheduleNotification() {
if shouldRemind && dueDate > Date() {
// 1
let content = UNMutableNotificationContent()
content.title = "Reminder:"
content.body = text
content.sound = UNNotificationSound.default
// 2
let calendar = Calendar(identifier: .gregorian)
let components = calendar.dateComponents(
[.year, .month, .day, .hour, .minute],
from: dueDate)
// 3
let trigger = UNCalendarNotificationTrigger(
dateMatching: components,
repeats: false)
// 4
let request = UNNotificationRequest(
identifier: "\(itemID)",
content: content,
trigger: trigger)
// 5
let center = UNUserNotificationCenter.current()
center.add(request)
print("Scheduled: \(request) for itemID: \(itemID)")
}
}
You’ve seen this code before when you tried out local notifications for the first time, but there are a few differences.
-
Put the item’s
textinto the notification message. -
Extract the year, month, day, hour, and minute from the
dueDate. We don’t care about the number of seconds — the notification doesn’t need to be scheduled with up-to-the-second precision, on the minute is precise enough. -
To test local notifications you used a
UNTimeIntervalNotificationTrigger, which scheduled the notification to appear after a number of seconds. Here, you’re using aUNCalendarNotificationTrigger, which shows the notification at the specified date. -
Create the
UNNotificationRequestobject. Important here is that we convert the item’s numeric ID into aStringand use it to identify the notification. That is how you’ll be able to find this notification later in case you need to cancel it. -
Add the new notification to the
UNUserNotificationCenter.
Xcode is not so impressed with this new code and gives a bunch of error messages.
What is wrong here? UNUserNotificationCenter and the other objects are provided by the User Notifications framework — you can tell by the “UN” prefix in their names.
However, ChecklistItem hasn’t used any code from that framework until now. The only framework objects it has used, NSObject and Codable, came from another framework, Foundation.
➤ To tell ChecklistItem about the User Notifications framework, you need to add the following line to the top of the file, below the other import:
import UserNotifications
Now the errors disappear like snow in the sun.
There’s another small problem, though. If you’ve reset the Simulator recently, then the app no longer has permission to send local notifications.
➤ Try it out. Run the app, add a new checklist item, set the due date a minute into the future, and press Done. You might not see a notification.
Even if you do see a notification, since the authorization request code is no longer there, Checklists certainly won’t have permission to display notifications on your user devices.
When you were just messing around at the beginning of this chapter, you placed the permission request code in the AppDelegate and ran it immediately upon launch.
That’s not recommended.
Don’t you just hate those apps that prompt you for ten different things before you’ve even had a chance to properly look at them? Let’s be a bit more user friendly with our own app!
➤ Add the following method to ItemDetailViewController.swift:
@IBAction func shouldRemindToggled(_ switchControl: UISwitch) {
textField.resignFirstResponder()
if switchControl.isOn {
let center = UNUserNotificationCenter.current()
center.requestAuthorization(options: [.alert, .sound]) {_, _ in
// do nothing
}
}
}
When the Remind Me switch is toggled to ON, this prompts the user for permission to send local notifications. Once the user has given permission, the app won’t put up a prompt again.
➤ Also add an import UserNotifications or the above method might not compile.
➤ Open the storyboard and connect the shouldRemindToggled: action to the switch control.
➤ Test it out. Run the app, add a new checklist item, set the due date a minute into the future, press Done and exit to the home screen.
Wait one minute (patience…) and the notification should appear. Pretty cool!
That takes care of the adding a new item scenario. There are two others left.
Edit an existing item
When the user edits an item, the following situations can occur with the Remind Me switch:
- Remind Me was switched off and is now switched on. You have to schedule a new notification.
- Remind Me was switched on and is now switched off. You have to cancel the existing notification.
- Remind Me stays switched on but the due date changes. You have to cancel the existing notification and schedule a new one.
- Remind Me stays switched on but the due date doesn’t change. You don’t have to do anything.
- Remind Me stays switched off. Here you also don’t have to do anything.
Of course, in all those situations you’ll only schedule the notification if the due date is in the future.
Phew, that’s quite a list. It’s always a good idea to take stock of all possible scenarios before you start programming because this gives you a clear picture of everything you need to tackle.
It may seem like you need to write a lot of logic here to deal with all these situations, but actually it turns out to be quite simple.
First you’ll check if there is an existing notification for this to-do item. If there is, you simply cancel it. Then you determine whether the item should have a notification and if so, you schedule a new one.
That should take care of all the above situations, even if sometimes you simply could have left the existing notification alone. The algorithm is crude, but effective.
➤ Add the following method to ChecklistItem.swift:
func removeNotification() {
let center = UNUserNotificationCenter.current()
center.removePendingNotificationRequests(withIdentifiers: ["\(itemID)"])
}
This removes the local notification for this ChecklistItem, if it exists. Note that removePendingNotificationRequests() requires an array of identifiers, so we first put our itemID into a string with \(…) and then into an array using [].
➤ Call this new method from to the top of scheduleNotification():
func scheduleNotification() {
removeNotification()
. . .
}
Let’s try it out.
➤ Run the app and add a to-do item with a due date two minutes into the future. A new notification will be scheduled. Go to the home screen and wait until it shows up.
➤ Edit the item and change the due date to three minutes into the future. The old notification will be removed and a new one scheduled for the new time.
➤ Add a new to-do item with a due date two minutes into the future. Edit the to-do item but now set the switch to OFF. The old notification will be removed and no new notification will be scheduled.
➤ Edit again and put the time a few minutes into the future but don’t change anything else; no new notification will be scheduled because the switch is still off.
These tests should also work if you terminate the app in between.
Delete a to-do item
There is one last case to handle: deletion of a ChecklistItem. This can happen in two ways:
- The user can delete an individual item using swipe-to-delete.
- The user can delete an entire checklist, in which case all its
ChecklistItemobjects are also deleted.
An object is notified when it is about to be deleted using the deinit message. You can simply implement this method, check if there is a scheduled notification for this item, and then cancel it.
➤ Add the following to ChecklistItem.swift:
deinit {
removeNotification()
}
That’s all you have to do. The special deinit method will be invoked when you delete an individual ChecklistItem but also when you delete a whole Checklist — because all its ChecklistItems will be destroyed as well, as the array they are in is deallocated.
➤ Run the app and try it out. First, schedule some notifications a minute or so into the future and then remove that to-do item or its entire checklist. Wait until the due date comes and you shouldn’t get a notification.
Once you’re convinced everything works, you can remove the print() statements. They are only temporary for debugging purposes. You probably don’t want to leave them in the final app. The print() statements won’t hurt, but the end user can’t see those messages anyway.
➤ Also remove the item ID from the label in the ChecklistViewController – that was only used for debugging.
That’s a wrap!
Things should be starting to make sense by now. I’ve thrown you into the deep end by writing an entire app from scratch. We’ve touched on a number of advanced topics already, but hopefully you were able to follow along quite well with what we’ve been doing. Kudos for sticking with it until the end!
It’s OK if you’re still a bit fuzzy on the details. Sleep on it for a bit and keep tinkering with the code. Programming requires its own way of thinking and you won’t learn that overnight. Don’t be afraid to do this app again from the start — it will make more sense the second time around!
This section focused mainly on UIKit and its most important controls and patterns. In the next section we’ll take a few steps back to talk more about the Swift language itself. And of course, you’ll build another cool app.
Here is the final storyboard for Checklists:
Completing all of that is pretty impressive! Give yourself a well-deserved pat on the back :]
Take a break, and when you’re ready, continue on to the next section, where you’ll make an app that knows its place! :-)
Haven’t had enough yet? Here are some challenges to sink your teeth into:
Exercise: Display the due date in the table view cells, under the text of the to-do item.
Exercise: Sort the to-do items list based on the due date. This is similar to what you did with the list of
Checklists except that now you’re sortingChecklistItemobjects and you’ll be comparingDateobjects instead of strings.
You can find the final project files for the Checklists app under 20-Local-notifications in the Source Code folder.