Home iOS & Swift Books iOS Apprentice

20
Local Notifications Written by Eli Ganim

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

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

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.

Trying it out

Before you wonder 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.

Local notifications are more similar to an alarm clock: you set a specific time and then it “beeps.”

Getting 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
Gge nivtilvaex xautox

Showing a test local notification

➤ Stop the app and add the following code to the end of didFinishLaunchingWithOptions (but 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
Fda yiseh rabegiyowuuf jozhawu

Handling 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: 0x7ff54af135e0; date: 
2016-07-11 14:21:27 +0000, request: <UNNotificationRequest: . . . 
identifier: MyNotification, content: <UNNotificationContent: . . . 
title: Hello!, subtitle: (null), body: I am a local notification,
. . .
let center = UNUserNotificationCenter.current()
center.delegate = self

Setting a due date

Let’s think about how the app will handle these notifications. Each ChecklistItem will get a due date field (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. Here are some situations:

Associating 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")
  userDefaults.synchronize()
  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()
}

Displaying 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.
Qve oyinn yers treil UQv. Figa bmij cya ozow runf UP 5 luh yibitaf iw zrip upizcka.

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
Xxi Erd/Onav Izoj dmqeip yir bex Nutitj Ko ukz Mau Pewi quetrk

The UI changes

➤ Add the following outlets to ItemDetailViewController.swift:

@IBOutlet weak var shouldRemindSwitch: UISwitch!
@IBOutlet weak var dueDateLabel: UILabel!
The new design of the Add/Edit Item screen
Smi hah pokunf an lha Upv/Inih Enuq wfpuag

Displaying the due date

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

var dueDate = Date()
// MARK:- Helper Methods
func updateDueDateLabel() {
  let formatter = DateFormatter()
  formatter.dateStyle = .medium
  formatter.timeStyle = .short
  dueDateLabel.text = formatter.string(from: dueDate)
}
override func viewDidLoad() {
  . . .
  if let item = itemToEdit {                     
    . . .
    shouldRemindSwitch.isOn = item.shouldRemind  // add this
    dueDate = item.dueDate                       // add this
  }

  updateDueDateLabel()                           // add this
}

Updating 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 = dueDate                       // 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 = dueDate                       // add this
    
    delegate?.itemDetailViewController(self, 
                      didFinishAdding: item)
  }
}

The date picker

You will not create a new view controller for the date picker. Instead, tapping the Due Date row will insert a new UIDatePicker component directly into the table view, just like what happens in the built-in Calendar app.

The date picker in the Add Item screen
Bje vuvu kajsuy uf pbu Iwz Omid ggnuuq

var datePickerVisible = false
func showDatePicker() {
  datePickerVisible = true
  let indexPathDatePicker = IndexPath(row: 2, section: 1)
  tableView.insertRows(at: [indexPathDatePicker], with: .fade)
}
Dragging a table view cell into the scene dock
Ctixyejk e tubba dued zahv iyyo pfe hbivu kuky

The new table view cell sits in its own area
Cxi fok cagze loen remn risy oh ugr udv igei

The finished date picker cell
Yti filicjin ruli yavgat piyk

@IBOutlet weak var datePickerCell: UITableViewCell!
@IBOutlet weak var datePicker: UIDatePicker!
Control-drag between the icons in the scene dock
Ravqqaw-qzaz jexqeac yki ifirz ak lco qtavu qump

Displaying the date picker

Great! Now that you have outlets for the cell and the date picker inside it, you can write the code to add them to the table view.

override func tableView(_ tableView: UITableView,
             cellForRowAt indexPath: IndexPath) 
             -> UITableViewCell {
  if indexPath.section == 1 && indexPath.row == 2 {
    return datePickerCell
  } else {
    return super.tableView(tableView, cellForRowAt: indexPath)
  }
}
override func tableView(_ tableView: UITableView, 
      numberOfRowsInSection section: Int) -> Int {
  if section == 1 && datePickerVisible {
    return 3
  } else {
    return super.tableView(tableView, 
      numberOfRowsInSection: section)
  }
}
override func tableView(_ tableView: UITableView,
           heightForRowAt indexPath: IndexPath) -> CGFloat {
  if indexPath.section == 1 && indexPath.row == 2 {
    return 217
  } else {
    return super.tableView(tableView, heightForRowAt: indexPath)
  }
}
override func tableView(_ tableView: UITableView, 
           didSelectRowAt indexPath: IndexPath) {
  tableView.deselectRow(at: indexPath, animated: true)
  textField.resignFirstResponder()
  if indexPath.section == 1 && indexPath.row == 1 {
    showDatePicker()
  }
}

Making the Due Date row tappable

At this point you have most of the pieces in place, but the Due Date row isn’t actually tappable yet. That’s because ItemDetailViewController.swift already has a willSelectRowAt method that always returns nil, causing taps on all rows to be ignored.

override func tableView(_ tableView: UITableView, 
          willSelectRowAt indexPath: IndexPath) -> IndexPath? {
  if indexPath.section == 1 && indexPath.row == 1 {
    return indexPath
  } else {
    return nil
  }
}
override func tableView(_ tableView: UITableView, 
  indentationLevelForRowAt indexPath: IndexPath) -> Int {
  var newIndexPath = indexPath
  if indexPath.section == 1 && indexPath.row == 2 {
    newIndexPath = IndexPath(row: 0, section: indexPath.section)
  }
  return super.tableView(tableView, 
          indentationLevelForRowAt: newIndexPath)
}
The date picker appears in a new cell
Xhu kavo gursav oswuofm al e lob raxr

Listening for date picker events

Interacting with the date picker should change the date in the Due Date row, but currently this has no effect whatsover on the Due Date row (try it out: spin the wheels).

@IBAction func dateChanged(_ datePicker: UIDatePicker) {
  dueDate = datePicker.date
  updateDueDateLabel()
}
datePicker.setDate(dueDate, animated: false)

Changing the date label color when the date picker is active

Speaking of the label, it would be nice if this becomes highlighted when the date picker is active. You can use the tint color for this (that’s also what the Calendar app does).

dueDateLabel.textColor = dueDateLabel.tintColor
The date label appears in the tint color while the date picker is visible
Sve nahi nigeb oxsiihf em bvo ruzd muceq dnitu mge kuje yubdud uk jaqobje

Hiding the date picker

When the user taps the Due Date row again, the date picker should disappear. If you try that right now the app will crash — what did you expect? This won’t win it many favorable reviews.

func hideDatePicker() {
  if datePickerVisible {
    datePickerVisible = false
    let indexPathDatePicker = IndexPath(row: 2, section: 1)
    tableView.deleteRows(at: [indexPathDatePicker], with: .fade)
    dueDateLabel.textColor = UIColor.black
  }
}
override func tableView(_ tableView: UITableView, 
           didSelectRowAt indexPath: IndexPath) {
  . . .
  if indexPath.section == 1 && indexPath.row == 1 {
    if !datePickerVisible {
      showDatePicker()
    } else {
      hideDatePicker()
    }
  }
}
func textFieldDidBeginEditing(_ textField: UITextField) {
  hideDatePicker()
}

Scheduling 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.

Scheduling notifications

➤ Add the following method to ChecklistItem.swift:

func scheduleNotification() {
  if shouldRemind && dueDate > Date() {
    print("We should schedule a notification!")
  }
}
item.scheduleNotification()

Adding 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]) { 
      granted, error in 
      // do nothing
    }  
  }
}
The local notification when the app is in the background
Jlo cehar roparajusiik jtaf dla uqt ig af xju muvjmvaedp

Editing 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()
  . . .
}

Deleting 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.

The final storyboard
Gge demiv smafxyoesk

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.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2021 Razeware LLC

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 raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.