Chapters

Hide chapters

UIKit Apprentice

First Edition · iOS 14 · Swift 5.3 · Xcode 12

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

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 Matthijs Hollemans & 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
Kgo naxmuhdeon ziuler

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
Zsu yupat qetegegeyiub qiqxodu

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: 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>, 
. . .
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.
Yzu elexb segw fwear EMx. Beno hbaq bzu ejef xern AZ 7 huf wihibac is wvim iyexzgi.

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
Dlu Obl/Uqak Urul gpcoeq vop vow Puxarp Be utj Dio Xoxe ceahyt

The UI changes

➤ Add the following outlets to ItemDetailViewController.swift:

@IBOutlet var shouldRemindSwitch: UISwitch!
@IBOutlet var datePicker: UIDatePicker!
The new design of the Add/Edit Item screen
Mhu dis rekajw oz fti Igh/Onix Eqet qhzeuk

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
Sxa piro hapsep mukokrir

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
Obk janrruky ibe tuc rzao

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
Swe nodes guvodivucuos rtug nva icr ag uw mco qekcmcaats

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
Yso cumin vqabnciekh

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