Chapter 11. Reminders and Notifications

In the last chapter we set up our settings view and wrote the code to let the user decide if they want to support location features in the app. Now we can add a new feature—a reminder to take a selfie each day that will display as a notification—and configure this to only be enabled based on the user choosing this setting. To do this we will need to write code to add a new row into the settings table view, write new code to generate a daily reminder notification, and enable and disable that reminder being fired based on the user’s selection in the settings.

Adding a Reminder to the Settings

The first thing we want to do is set up a new row in the settings table view that we can use for toggling the reminder on and off as needed:

  1. Open Main.storyboard.

  2. Select the table view section (not the row or table view itself) from the settings view controller.

    The easiest way to select the table view section is by using the outline, as shown in Figure 11-1.

    lsw3 1101
    Figure 11-1. Select the table view section from the outline
  3. Using the Attributes inspector, increase the number of rows from one to two. Doing this will create an exact duplicate of the location setting row, which will save us having to recreate the constraints. This does, however, have the drawback of also duplicating the actions, so we will have to change this if we don’t want to find ourselves very confused why turning off the reminder setting also turns off the location setting.

  4. Select the bottom cell’s label.

  5. Change the text to “Remind me at 10am”.

  6. Select the switch in the bottom cell.

  7. Inside the Connections inspector, break the connection from the Value Changed event in the Sent Events section by clicking the small x button. This will have the effect of unlinking this switch from the action we wrote in Chapter 10.

  8. Open the assistant editor.

  9. Inside the SettingsTableViewController class create a new outlet for the switch called reminderSwitch.

  10. Create a new action for the switch called reminderSwitchToggled.

We are now finished with our UI and can close the storyboard and assistant editor. The rest of what we are doing is going to be in code.

Creating a Notification

In Chapter 10 we used UserDefaults to maintain the state of the location setting. This time we are going to be doing something a little bit different. We will be creating a new user notification to actually show our reminder, and then we will be scheduling this reminder with the Notification Center, which will handle it from there. Because the Notification Center persists across multiple launches we can query it, asking if there is a reminder scheduled, and use that as the way of determining the appearance of the switch. This requires a bit more work than using UserDefaults, but we need to do the work anyway to configure the notification, so we might as well not add more work by also supporting UserDefaults for the reminder.

Note

We will be creating a local notification—that is, one that only exists in the device itself. The other kind is a push notification, which is sent to your device by a remote server. The basic configuration of both is the same, but push notifications require a lot more work to get up and running. For a start, you need to have them sent remotely to your device, adding another layer to worry about, and even once you’ve done that it doesn’t significantly change how you handle them in your app. For this reason, we aren’t going to cover push notifications. Apple has very good documentation on how you can use notifications in your app, and if you are curious as to how to get push notifications working it is worth checking out.

Let’s get started creating our notification:

  1. Open SettingsTableViewController.swift and include the UserNotifications module:

    import UserNotifications

    This will give us access to the UserNotifications library, which is what we will be using to create and present the notification.

  2. Add a new property to the SettingsTableViewController class:

    private let notificationId = "SelfiegramReminder"

    This will be used later by the notification system to uniquely identify our notification amongst all the others out there.

  3. Add the following to the reminderSwitchToggled method:

    @IBAction func reminderSwitchToggled(_ sender: Any)
    {
        // Get the Notification Center.
        let current = UNUserNotificationCenter.current()
    
        switch reminderSwitch.isOn
        {
        case true:
            // Defines what kinds of notifications we send --
            // in our case, a simple alert
            let notificationOptions : UNAuthorizationOptions = [.alert]
    
            // The switch was turned on.
            // Ask permission to send notifications.
            current.requestAuthorization(options: notificationOptions,
                               completionHandler: { (granted, error) in
                if granted
                {
                    // We've been granted permission. Queue the notification.
                    self.addNotificationRequest()
                }
    
                // Call updateReminderSwitch,
                // because we may have just learned that
                // we don't have permission to.
                self.updateReminderSwitch()
            })
        case false:
            // The switch was turned off.
            // Remove any pending notification request.
            current.removeAllPendingNotificationRequests()
        }
    }

    Here, we first get a reference to the current UserNotificationCenter. This is the singleton object that is responsible for managing user notifications. We then switch over the state of the reminder switch. If it is off, all we do is tell the Notification Center to cancel all unfired notifications, effectively purging our notification from the system. If the switch is on, however, we have to do a bit more. Like with location gathering, the user must grant the app permission to present notifications before we are allowed to do so, so we first have to check if we have permission to send notifications. Unlike with location, we use a closure to determine our authorization status. If permission is granted we then proceed to call a method to actually create the notification. If permission is denied we instead call a message to update the switch’s UI. Don’t worry about Xcode complaining about missing methods; we are going to be writing them now.

  4. Implement the addNotificationRequest method:

    func addNotificationRequest()
    {
        // Get the Notification Center
        let current = UNUserNotificationCenter.current()
    
        // Remove all existing notifications
        current.removeAllPendingNotificationRequests()
    
        // Prepare the notification content
        let content = UNMutableNotificationContent()
        content.title = "Take a selfie!"
    
        // Create date components to represent "10AM" (without
        // specifying a day)
        var components = DateComponents()
        components.setValue(10, for: Calendar.Component.hour)
    
        // A trigger that goes off at this time, every day
        let trigger = UNCalendarNotificationTrigger(dateMatching: components,
                                                    repeats: true)
    
        // Create the request
        let request = UNNotificationRequest(identifier: self.notificationId,
                                            content: content,
                                            trigger: trigger)
    
        // Add it to the Notification Center
        current.add(request, withCompletionHandler: { (error) in
            self.updateReminderSwitch()
        })
    }

    This method is the meat of the notification generation. First, we tell the Notification Center to purge all unfired notifications. This is to prevent the user toggling the reminder switch multiple times and at the next 10 a.m. being sent a whole heap of alerts to take a selfie.

    Then we create a notification content object, which we can configure however we like. In iOS you don’t directly create the notification you want to send; instead, you set up how you want it to work and let the Notification Center take care of it. In our case all we are doing is giving it a title telling our user to take a selfie, although you can do quite a bit more, including using custom sounds and images. Next, we set up a trigger, which will be used to determine when the notification is delivered. We have set it to go off at 10 a.m. each day by using a DateComponent.

    With our content and trigger ready, we make a notification request. This is what the Notification Center will use to create the notification and fire it at the appropriate time. The request is basically just a bundled-up form of our trigger (10 a.m. each day), our content (the title “Take a Selfie!”), and the identifier we set up earlier. Then we pass the request to the Notification Center, and it will take it from there. Once all this is done we call a method to update the switch UI, which we are about to write.

    When the appropriate moment is reached for the trigger to fire, the Notification Center will create a notification object with the content we set up and send it off.

  5. Implement the updateReminderSwitch method:

    func updateReminderSwitch()
    {
        UNUserNotificationCenter.current().getNotificationSettings
        { (settings) in
            switch settings.authorizationStatus
            {
            case .authorized:
                UNUserNotificationCenter.current()
                    .getPendingNotificationRequests(
                        completionHandler: { (requests) in
    
                    // We are active if the list of requests contains one
                    // that's got the correct identifier
                    let active = requests
                        .filter({ $0.identifier == self.notificationId })
                        .count > 0
    
                    // Our switch is enabled; it's on if we found our pending
                    // notification
                    self.updateReminderUI(enabled: true, active: active)
                })
    
            case .denied:
                // If the user has denied permission, the switch is off and
                // disabled
                self.updateReminderUI(enabled: false, active: false)
    
            case .notDetermined:
                // If the user hasn't been asked yet, the switch is enabled,
                // but defaults to off
                self.updateReminderUI(enabled: true, active: false)
            }
        }
    }

    This method is where we work out what state the reminder switch needs to be in—whether it’s on, based on if there is a reminder scheduled, and whether it should be enabled, based on the user’s permission setting for notifications. By determining if the switch should be enabled, we prevent it from being interactive even if the user has denied permission for notifications.

    First we determine the permissions from the Notification Center. If permission has been given, we enable the switch and then check if the Notification Center currently has a scheduled notification that matches the notification ID we created earlier. Depending on this, the switch will then be toggled to true or false. If permission has been denied we set the switch to false and disable it, preventing user interaction. Finally, if permission hasn’t yet been determined, we enable the switch but set it to false, allowing the first interaction with it to be possible.

  6. Implement the updateReminderUI(enabled: active:) method:

    private func updateReminderUI(enabled: Bool, active: Bool)
    {
        OperationQueue.main.addOperation {
            self.reminderSwitch.isEnabled = enabled
            self.reminderSwitch.isOn = active
        }
    }

    This is a convenience method that sets the toggle and enabled states of the switch to those determined in the updateReminderSwitch method. The only interesting part here is that we are wrapping the calls inside a call to the main queue. The reason for this is that we don’t know what queue the Notification Center will run the completion closure on when it completes adding a notification, but updates to the UI are only allowed on the main queue. Because of this we are making sure that changes to the state of our switch are explicitly run on the main queue.

  7. Add the following to the end of viewDidLoad to set the switch state on first launch of the view controller:

    updateReminderSwitch()

    With that done, Selfiegram is now ready to start showing notifications. If you run the app, jump into the settings view, and turn that switch to “on” (Figure 11-2) a notification will be scheduled to fire at 10 a.m.!

lsw3 1102
Figure 11-2. Showing off our snazzy new notification

With all of this done, we have now completed the basic version of Selfiegram. We started with nothing and have implemented an entire app that is ready for people to start using and testing. In the next chapters we are going to look at adding some polish and advanced functionality to our working app.