Chapters

Hide chapters

UIKit Apprentice

Second Edition · iOS 15 · Swift 5.5 · Xcode 13

My Locations

Section 3: 11 chapters
Show chapters Hide chapters

Store Search

Section 4: 13 chapters
Show chapters Hide chapters

20. Local Notifications
Written by Fahim Farook

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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.

import UserNotifications
// Notification authorization
let center = UNUserNotificationCenter.current()
center.requestAuthorization(options: [.alert, .sound]) {granted, error in
  if granted {
    print("We have permission")
  } else {
    print("Permission denied")
  }
}

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.

.badge
.sound
.alert
.carPlay
center.requestAuthorization(options: [UNAuthorizationOptions.alert, UNAuthorizationOptions.sound]) {
  . . .

The permission dialog
Jzo hejwaynaem reefob

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)
The local notification message
Myu yehok fotawezahuen durqiri

Handle local notification events

➤ Add the notification delegate to AppDelegate’s class declaration:

class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
// MARK: - User Notification Delegates
func userNotificationCenter(
  _ center: UNUserNotificationCenter,
  willPresent notification: UNNotification,
  withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
  print("Received local notification \(notification)")
}
center.delegate = self
Received local notification <UNNotification: 0x600000f9f4b0; source: com.razeware.Checklists date: 2021-07-17 19:37:44 +0000, request: <UNNotificationRequest: 0x600000f9f450; identifier: MyNotification, content: <UNNotificationContent: 0x600003de9900; title: <redacted>, subtitle: (null),
. . .
let center = UNUserNotificationCenter.current()
center.delegate = self

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.

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:

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.

var dueDate = Date()
var shouldRemind = false
var itemID = -1
class func nextChecklistItemID() -> Int {
  let userDefaults = UserDefaults.standard
  let itemID = userDefaults.integer(forKey: "ChecklistItemID")
  userDefaults.set(itemID + 1, forKey: "ChecklistItemID")
  return itemID
}

Class methods vs. instance methods

If you are wondering why you wrote:

class func nextChecklistItemID()
func nextChecklistItemID()
itemID = DataModel.nextChecklistItemID()
itemID = dataModel.nextChecklistItemID()

override init() {
  super.init()
  itemID = DataModel.nextChecklistItemID()
}

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.

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)"
}
The items with their IDs. Note that the item with ID 3 was deleted in this example.
Jna icomt vety spiux IWm. Fuvu zcek plu ados vazx UC 9 rej qilogez un zlis exuyxre.

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 Add/Edit Item screen now has Remind Me and Due Date fields
Szi Idb/Anik Imus qfviow sug ciw Cucihl Ti etm Lei Loja xeagcs

The UI changes

➤ Add the following outlets to ItemDetailViewController.swift:

@IBOutlet weak var shouldRemindSwitch: UISwitch!
@IBOutlet weak var datePicker: UIDatePicker!
The new design of the Add/Edit Item screen
Dfa kek goqelh av mne Otd/Iluc Arox ptqiuh

Display the due date

Let’s write the code for displaying the due date.

override func viewDidLoad() {
  . . .
  if let item = itemToEdit {
    . . .
    shouldRemindSwitch.isOn = item.shouldRemind  // add this
    datePicker.date = item.dueDate               // add this
  }
}

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)
  }
}
The date picker calendar
Nzi gebo mukdah gafeqliz

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.

All controls are now blue
Exz samsbimf udi vav hwou

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!")
  }
}
item.scheduleNotification()

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)")
  }
}
import UserNotifications
@IBAction func shouldRemindToggled(_ switchControl: UISwitch) {
  textField.resignFirstResponder()

  if switchControl.isOn {
    let center = UNUserNotificationCenter.current()
    center.requestAuthorization(options: [.alert, .sound]) {_, _ in
      // do nothing
    }
  }
}
The local notification when the app is in the background
Sli nemif nuxiwogibeab hwel vha erf ib ov hzu dobvrbuilt

Edit an existing item

When the user edits an item, the following situations can occur with the Remind Me switch:

func removeNotification() {
  let center = UNUserNotificationCenter.current()
  center.removePendingNotificationRequests(withIdentifiers: ["\(itemID)"])
}
func scheduleNotification() {
  removeNotification()
  . . .
}

Delete a to-do item

There is one last case to handle: deletion of a ChecklistItem. This can happen in two ways:

deinit {
  removeNotification()
}

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!

The final storyboard
Xha mofup qtihhraons

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now