Engage Users With Local Notifications (2023)

Our app can be of a great help to stay focused and completing tasks. But, if the user forgets to check back and closes the app, or if the app gets terminated by operating system, all the tasks will be lost:(.

Imagine, you spoke with other participants of the project, and decided that it would be beneficial to the user if the app could present reminders!

Discovering UI Notifications

Reminders can be implemented using notifications. This time - UI Notifications.UI Notifications arepresented to the user, while they are not using the app.

There are two types of notifications:

  • Local

  • Remote

Local notifications originate and are delivered on the same device. Like an event reminder from a calendar app, for example.

Remote notifications are also called Push notifications, that come from a server. Like a notification upon receiving an email or a text message.

Push Notifications can also be silent. Those will have no UI presence and are meant to just deliver some information for the app.

We'll adopt Local Notifications in our project to implement task reminders, so, let's GO :pirate:!

Implementing Local Notifications

In order to implement local notifications we need to accomplish the following:

  • Request permission

  • ScheduleNotifications

Whileworking on this matter, we'llhave a chance to work with the file we haven'ttouched yet - AppDelegate.swift. As you may guessmyit's name, it's an application delegate and the delegating 'object' is iOS itself.When iOS needs to collaborate with our application, this is the objectthat's a gatekeeper!

To be able to use user notifications functionality, import UserNotification framework in AppDelegate.swift:

import UserNotifications

Request Permission

Notifications are a delicatematter. Perhaps you experienced it yourself when an app on your device keeps sending annoying notifications or, perhaps at a wrongtime.:'(

Thankfully, as users, we can control if an app is allowed to displaynotifications and in what form. This means, as developers, we must ask for permission... ;)

So, our implementation starts with requestingpermissions. In AppDelegate.swiftthis is done within a app delegate method application/didFinishLaunchingWithOptions - the method is already there for us, we just need to add our code there:

 func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (granted, error) in if (granted) { print ("We'll be able to set Hot Reminders!") } else { print ("We need to prove the app amazing so the user will change their mind!") } } return true }

Let's review that magical single line of code:

  • We are accessing UNUserNotificationCenter object and its currentrepresentation available for the app(by the way, UN prefix stands for User Notificationsand indicates thatthe name comes fromUserNotifications framework)

  • CallingrequestAuthorizationon the current User Notification Center object. Thismethod triggers an alert view, which asks the user whether they will allow the app to send notifications.

Let's now take a closer look at the parameters of the method:

  • The first parameter takes an array of options. These options indicate which kind of notifications your app is using.Here are the available options:

    • badge

    • sound

    • alert

    • carPlay

  • The second parameter is a hander block, that's called after the user has responded to the alert view. Thegrantedparameter tells us whether the user allowed notifications or not.

After completion of this request, if the user gave the permission, our app isready to schedule notifications, if not - we need to impress the user with the app functionality, so that they change their mind.

Schedule Notifications

Sending local notifications incorporate three components:

  • acontent

  • a trigger

  • a request.

We'll be generating itwithour view controller code, so let'simport the UserNotificationsframework to ViewController.swift:

import UserNotifications

And create a helper method to manage the localnotifications business:

func manageLocalNotifications() { }

Then,we need to call it every time we perform some actions with tasks. Since we are providing sample notificationsto start, we'll alsocall this method in our appconfiguration method:

 func configure() { tableView.delegate = self tableView.dataSource = self populateInitialTasks() updateProgress() registerForKeyboardNotifications() manageLocalNotifications() }

We'll add it to the rest of the code shortly.

Let’s start by looking at the content and create a sample object within the new method:

let content = UNMutableNotificationContent()content.title = "Title"content.body = "Body"content.sound = UNNotificationSound.default()

We're creating anobject of UNMutableNotificationContent class and configuring its essential properties - title and body and assigning a default notification sound to its respective property.

Next, we need to create a trigger object - UNTimeIntervalNotificationTrigger :

let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)

The initializer of UNTimeIntervalNotificationTrigger takes two parameters - time interval that needs to pass from the timeof scheduling until the notification needs to popup. We'll use 5 seconds for testing. The second parameter indicateswhether the notificationneeds to be repeated - we'll have a one time reminderfor the moment.

Last object to create is arequest object - UNNotificationRequest :

let request = UNNotificationRequest(identifier: "TestIdentifier", content: content, trigger: trigger)

The initializer of UNNotificationRequest takes 3 parameters- the identifier- what we can use to access thatnotification at a later time, and the 2 objects we just created - content and trigger.

Pretty straight forward so far!:zorro:

And finally, we are ready to schedule our notifications:

UNUserNotificationCenter.current().add(request, withCompletionHandler:nil)

So the complete code of manageNotifications()method looks like this:

 func manageLocalNotifications() { // create content let content = UNMutableNotificationContent() content.title = "Title" content.body = "Body" content.sound = UNNotificationSound.default() // create trigger let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false) // create request let request = UNNotificationRequest(identifier: "TestIdentifier", content: content, trigger: trigger) // schedule notification UNUserNotificationCenter.current().add(request, withCompletionHandler:nil) }

Let'stest it! Remember, the notifications are only displayed when the user is NOT using the app! So, after you launch the app in simulator and after giving permission to send notifications,click home button to observe the notification appear:

Engage Users With Local Notifications (1)

Now let'simplement the actual content. We'll be creating a single reminder notification that will display hot tasksprogress similarly to how wereport it on the table header within theapp. To optimize the code, let'smove out the notification schedulingfunctionality into a separate helper method:

func scheduleLocalNotification(title: String?, body: String?) { let identifier = "HostListSummary" let notificationCenter = UNUserNotificationCenter.current() // remove previously scheduled notifications notificationCenter.removeDeliveredNotifications(withIdentifiers: [identifier]) if let newTitle = title, let newBody = body { // create content let content = UNMutableNotificationContent() content.title = newTitle content.body = newBody content.sound = UNNotificationSound.default() // create trigger let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false) // create request let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger) // schedule notification notificationCenter.add(request, withCompletionHandler:nil) } }

Let's review the alterations:

  • The function is taking 2 parameters - those are the items we need for the content.

  • Since we are about to set an updated notification, we need to remove all that were previously scheduled and not yet delivered.We are using removeDeliveredNotifications/withIdentifiers method of Notification Center for that.

  • We're checking whether either of the parameters is nil and using it as an indication that we only needed to remove previous reminders.Otherwise proceed withscheduling a new one.

  • The rest of the content remains the same from our test example, except we replaced the titleand body temporary content with the parameters.

Now let's compose the real content for the reminder:

func manageLocalNotifications() { // prepare content let totalTasks = priorityTasks.count + bonusTasks.count let completedTasks = priorityTasks.filter { (task) -> Bool in return task.completed == true }.count + bonusTasks.filter { (task) -> Bool in return task.completed == true }.count var title: String? var body: String? if totalTasks == 0 { // no tasks title = "It's lonely here" body = "Add some tasks!" } else if completedTasks == 0 { // nothing completed title = "Get started!" body = "You've got \(totalTasks) hot tasks to go!" } else if completedTasks < totalTasks { // completedTasks less totalTasks title = "Progress in action!" body = "\(completedTasks) down \(totalTasks - completedTasks) to go!" } // schedule (or remove) reminders scheduleLocalNotification(title: title, body: body) }

Let's review the view functionality:

  • We are calculating the total number of tasks and number of completed ones.

  • Declaring optional variables to hold title and body.

  • Analyzing the numbers and either composing suitable title and body or leaving them nil.In case of nil valueswe areexpecting the reminders to be removed if any were scheduled earlier and not yet delivered.

Let's test our implementation! Here itis:

Engage Users With Local Notifications (2)

Set on Repeat!

Now that we have evidence that ourreminders work, let's set them on repeat! - every 2 hours!

So, let's alter the trigger creation:

let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 120, repeats: true)

AndcallmanageLocalNotifications method in all the spots related to the task management within the app:

  • Move and deletetask in editActionsForRowAt:

     let delete = UITableViewRowAction(style: .destructive, title: "Delete") { (action, indexPath) in self.deleteTask(at: indexPath) self.manageLocalNotifications() self.updateProgress() } let move = UITableViewRowAction(style: .normal, title: moveCaption) { (action, indexPath) in task.priority = (task.priority == .top) ? .bonus : .top self.deleteTask(at: indexPath) self.insertTask(task, at: moveToIndexPath) }
  • Add new task innewTaskCell delegate method:

     func newTaskCell(_ cell: NewTaskTableViewCell, newTaskCreated caption: String) { // create new HOT task:) let newTask = HotTask.init(caption: caption) // insert a new row in the beginning of priority section insertTask(newTask, at: IndexPath(row: 0, section: 1)) manageLocalNotifications() updateProgress() }
  • Task completion state changed in taskCell delegate method:

     func taskCell(_ cell: TaskTableViewCell, completionChanged completion: Bool) { // identify indexPath for a cell if let indexPath = tableView.indexPath(for: cell) { // fetch datasource for indexPath if let task = hotTaskDataSource(indexPath: indexPath) { // update the completion state task.completed = completion manageLocalNotifications() updateProgress() } } }

And here's the final code of the view controller:

import UIKitimport UserNotificationsclass ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, TaskCellDelegate, NewTaskCellDelegate { @IBOutlet weak var tableView: UITableView! @IBOutlet weak var progressLabel: UILabel! var priorityTasks = [HotTask]() var bonusTasks = [HotTask]() // MARK: Cells delegates func newTaskCell(_ cell: NewTaskTableViewCell, newTaskCreated caption: String) { // create new HOT task:) let newTask = HotTask.init(caption: caption) // insert a new row in the beginning of priority section insertTask(newTask, at: IndexPath(row: 0, section: 1)) manageLocalNotifications() updateProgress() } func taskCell(_ cell: TaskTableViewCell, completionChanged completion: Bool) { // identify indexPath for a cell if let indexPath = tableView.indexPath(for: cell) { // fetch datasource for indexPath if let task = hotTaskDataSource(indexPath: indexPath) { // update the completion state task.completed = completion manageLocalNotifications() updateProgress() } } } // MARK: Keyboard management func registerForKeyboardNotifications() { NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: .UIKeyboardWillShow, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: .UIKeyboardWillHide, object: nil) } @objc func keyboardWillShow(notification: NSNotification) { let keyboardFrame = notification.userInfo![UIKeyboardFrameEndUserInfoKey] as! CGRect adjustLayoutForKeyboard(targetHeight: keyboardFrame.size.height) } @objc func keyboardWillHide(notification: NSNotification){ adjustLayoutForKeyboard(targetHeight: 0) } func adjustLayoutForKeyboard(targetHeight: CGFloat) { tableView.contentInset.bottom = targetHeight } // MARK: Notifications func manageLocalNotifications() { // prepare content let totalTasks = priorityTasks.count + bonusTasks.count let completedTasks = priorityTasks.filter { (task) -> Bool in return task.completed == true }.count + bonusTasks.filter { (task) -> Bool in return task.completed == true }.count var title: String? var body: String? if totalTasks == 0 { // no tasks title = "It's lonely here" body = "Add some tasks!" } else if completedTasks == 0 { // nothing completed title = "Get started!" body = "You've got \(totalTasks) hot tasks to go!" } else if completedTasks < totalTasks { // completedTasks less totalTasks title = "Progress in action!" body = "\(completedTasks) down \(totalTasks - completedTasks) to go!" } // schedule (or remove) reminders scheduleLocalNotification(title: title, body: body) } func scheduleLocalNotification(title: String?, body: String?) { let identifier = "HostListSummary" let notificationCenter = UNUserNotificationCenter.current() // remove previously scheduled notifications notificationCenter.removeDeliveredNotifications(withIdentifiers: [identifier]) if let newTitle = title, let newBody = body { // create content let content = UNMutableNotificationContent() content.title = newTitle content.body = newBody content.sound = UNNotificationSound.default() // create trigger let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 120, repeats: true) // create request let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger) // schedule notification notificationCenter.add(request, withCompletionHandler:nil) } } // MARK: TableView delegates func numberOfSections(in tableView: UITableView) -> Int { return 3 } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { switch section { case 0: return 1 case 1: return priorityTasks.count case 2: return bonusTasks.count default: return 0 } } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { switch indexPath.section { case 0: let cell = tableView.dequeueReusableCell(withIdentifier: "NewTaskCellID", for: indexPath) as! NewTaskTableViewCell cell.delegate = self return cell case 1: let cell = tableView.dequeueReusableCell(withIdentifier: "TaskCellID", for: indexPath) as! TaskTableViewCell let task = hotTaskDataSource(indexPath: indexPath) cell.setCaption(task?.caption) cell.delegate = self return cell case 2: let cell = tableView.dequeueReusableCell(withIdentifier: "TaskCellID", for: indexPath) as! TaskTableViewCell let task = hotTaskDataSource(indexPath: indexPath) cell.setCaption(task?.caption) cell.delegate = self return cell default: return UITableViewCell.init() } } func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { switch section { case 1: return "Top Priority" case 2: return "Bonus" default: return nil } } func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { return nil } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.cellForRow(at: indexPath)?.accessoryType = .checkmark } func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) { tableView.cellForRow(at: indexPath)?.accessoryType = .none } func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { switch indexPath.section { case 0: return false default: return true } } func insertTask(_ task: HotTask?, at indexPath: IndexPath?) { if let task = task, let indexPath = indexPath { // put the table view in updating mode tableView.beginUpdates() // add new object to the datasource array if (indexPath.section == 1) { priorityTasks.insert(task, at: indexPath.row) } else { bonusTasks.insert(task, at: indexPath.row) } // insert a new cell to the table tableView.insertRows(at: [indexPath], with: .automatic) // finish updating table tableView.endUpdates() } } func deleteTask(at indexPath: IndexPath?) { if let indexPath = indexPath { // put the table view in updating mode tableView.beginUpdates() // add new object to the datasource array if (indexPath.section == 1) { priorityTasks.remove(at: indexPath.row) } else { bonusTasks.remove(at: indexPath.row) } // insert a new cell to the table tableView.deleteRows(at: [indexPath], with: .automatic) // finish updating table tableView.endUpdates() } } func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? { var actions: [UITableViewRowAction]? var moveCaption: String? var moveToIndexPath: IndexPath? switch indexPath.section { case 1: moveCaption = "Move to Bonus" moveToIndexPath = IndexPath(row: 0, section: 2) case 2: moveCaption = "Move to Priority" moveToIndexPath = IndexPath(row: 0, section: 1) default: return actions } if let task = hotTaskDataSource(indexPath: indexPath) { let delete = UITableViewRowAction(style: .destructive, title: "Delete") { (action, indexPath) in self.deleteTask(at: indexPath) self.manageLocalNotifications() self.updateProgress() } let move = UITableViewRowAction(style: .normal, title: moveCaption) { (action, indexPath) in task.priority = (task.priority == .top) ? .bonus : .top self.deleteTask(at: indexPath) self.insertTask(task, at: moveToIndexPath) } actions = [delete, move] } return actions } func hotTaskDataSource(indexPath: IndexPath) -> HotTask? { switch indexPath.section { case 1: return priorityTasks[indexPath.row] case 2: return bonusTasks[indexPath.row] default: return nil } } func updateProgress() { // calculate the initial values for task count let totalTasks = priorityTasks.count + bonusTasks.count let completedTasks = priorityTasks.filter { (task) -> Bool in return task.completed == true }.count + bonusTasks.filter { (task) -> Bool in return task.completed == true }.count // declare a caption variable var caption = "What's going on?!" // handle range possible scenarios if totalTasks == 0 { // no tasks caption = "It's lonely here - add some tasks!" } else if completedTasks == 0 { // nothing completed caption = "Get started - \(totalTasks) to go!" } else if completedTasks == totalTasks { // all completed caption = "Well done - \(totalTasks) completed!" } else { // completedTasks less totalTasks caption = "\(completedTasks) down \(totalTasks - completedTasks) to go!" } // assign the progress caption to the label progressLabel.text = caption } func populateInitialTasks() { priorityTasks.removeAll() priorityTasks.append(HotTask.init(caption: "Pickup MacBook Pro from Apple store")) priorityTasks.append(HotTask.init(caption: "Practice Japanese")) priorityTasks.append(HotTask.init(caption: "Buy ingredients for a cake for Alie's bday")) bonusTasks.removeAll() let hotTask = HotTask.init(caption: "Shop for funnky socks") hotTask.priority = .bonus bonusTasks.append(hotTask) } func configure() { tableView.delegate = self tableView.dataSource = self populateInitialTasks() updateProgress() registerForKeyboardNotifications() manageLocalNotifications() } override func viewDidLoad() { super.viewDidLoad() configure() } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. }}

Well Done!

Well done competing another iOS app!While creatingit, we've learned common elements of any iOS app!

Let's Recap!

Top Articles
Latest Posts
Article information

Author: Reed Wilderman

Last Updated: 01/15/2023

Views: 6466

Rating: 4.1 / 5 (72 voted)

Reviews: 95% of readers found this page helpful

Author information

Name: Reed Wilderman

Birthday: 1992-06-14

Address: 998 Estell Village, Lake Oscarberg, SD 48713-6877

Phone: +21813267449721

Job: Technology Engineer

Hobby: Swimming, Do it yourself, Beekeeping, Lapidary, Cosplaying, Hiking, Graffiti

Introduction: My name is Reed Wilderman, I am a faithful, bright, lucky, adventurous, lively, rich, vast person who loves writing and wants to share my knowledge and understanding with you.