Local Notifications

Local Notifications are scheduled and fired inside the app. If a user adds a new task to his To-Do list, we can remind him of it. The mechanism is very easy - we create notification object, we set its date and text, and iOS will show it when the time will come.

But before showing any notification to the user, we first need to ask his permission to do it. You can ask for it whenever you want - some people add this code to their App Delegate, so app asks user right on app's first launch. However it would be a better idea to tell the user why and when notifications will be used, and only then show them notification permissions dialog.

Since permissions dialog can't be modified, you can also show a friendly warning that iOS will show Notifications Dialog now. Also, if the user did not allow the app to show him notifications once, we won't be able to display the dialog later. The friendly warning can be shown and declined many times since it is not a real permissions dialog.

In the superapp, we are going to show local notifications three times per day. We want to remind the user to track his mood in the morning, in the afternoon, and in the evening.

To be polite, we will ask the user if he is fine with allowing us to send notifications. If it is fine, we will show him iOS Notifications Permissions dialog, and will never show him these dialogs again. If it is not - we will show this dialog next time the user loads the app. Let's do it for now in HomeScreen's on_load:

        class HomeScreen < PM::Screen
          title "Home"
          tab_bar_item title: "Home", item: "images/home"
          def on_load
            self.view.backgroundColor = UIColor.yellowColor

            check_local_notifications
          end

          def check_local_notifications
            # get current app notifications settings:
            app = UIApplication.sharedApplication
            notification_types = app.currentUserNotificationSettings.types

            # check if user allowed us to send him notifications
            not_allowed = notification_types == UIUserNotificationTypeNone
            permanently_disabled = NSUserDefaults["notifications_disabled"]

            if not_allowed && permanently_disabled != true
              UIAlertController.alert(self, "Notification Permissions",
                message: "We would like to remind you to track your mood. To do it, we need your permission to send you notifications.",
                buttons: ["Don't allow", "Allow"]) do |tapped_button|
                  if tapped_button == "Allow"
                    # what kind of notifications we want to show:
                    types = UIUserNotificationTypeBadge | UIUserNotificationTypeSound | UIUserNotificationTypeAlert

                    settings = UIUserNotificationSettings.settingsForTypes types, categories: nil

                    app.registerUserNotificationSettings settings
                  end
              end
            end
          end
        end
      
Unfortunately, not_allowed variable can be true in 2 cases: the user did not yet allowed us to show him notifications, or user has denied us to show him notifications. Unfortunately there is no straightforward way to make sure whether user denied notifications or app did not even asked for them yet.

If the user has denied notifications, iOS will never show permissions dialog again, and we will show our custom warning for nothing - even if the user taps "Allow", iOS won't show this dialog again.

To determine whether user denied notifications we use NSUserDefaults - a place where we can persist some data. registerUserNotificationSettings method will call a delegate method in the AppDelegate:

application(app, didRegisterUserNotificationSettings: settings)
If the user has not allowed the app to receive notifications, settings.types will be equal UIUserNotificationTypeNone. This info will be enough to understand that Notifications were denied, and we will save this state in NSUserDefaults["notifications_disabled"]:
        def application(app, didRegisterUserNotificationSettings: settings)
          if settings.types == UIUserNotificationTypeNone
            # user did not allow us to show notifications:
            NSUserDefaults["notifications_disabled"] = true
          else
            NSUserDefaults["notifications_disabled"] = false
          end
        end
      

Now when we finally can show notifications, let's talk how they work with the app in different states.

When your app is in the foreground (user currently uses it), and notification arrives, AppDelegate will fire a method application(app, didReceiveLocalNotification: notification). There you can tell the app what should it do. When the app is in the foreground, no sound or banners will be shown, and badges won't be updated.

If device's screen is turned off at the moment when a notification arrives, iOS will show the notification with your text and app's icon on its Lock Screen.

If the user has another app opened at the moment when a notification arrives, iOS will show a banner at the top of the screen with your notification's text.

Scheduling

Let's make our HomeScreen create notifications for next three days. We will do it only if user has notifications enabled:

        def check_local_notifications
          app = UIApplication.sharedApplication
          notification_types = app.currentUserNotificationSettings.types
          not_allowed = notification_types == UIUserNotificationTypeNone
          allow_tapped = NSUserDefaults["allow_tapped"]

          if not_allowed && !allow_tapped
            # code to show dialog
          else
            # set new notifications for next 3 days, including today:
            now = NSDate.now
            date_components = { year: now.year, month: now.month }
            hours = [9, 14, 20]

            # 3 days, 3 notifications per day:
            3.times do |i|
              day = NSDate.now + i.day
              date_components[:day] = day.day

              hours.each do |hour|
                date_components[:hour] = hour
                date = NSDate.from_components date_components
                create_notification(date)
              end
            end
          end
        end

        def create_notification(date)
          # we don't want any notifications in the past:
          return if date < NSDate.now

          # check if this is a new notification:
          app = UIApplication.sharedApplication
          notifications = app.scheduledLocalNotifications
          same_date_notification = notifications.find do |existed_notification|
            existed_notification.fireDate == date
          end

          # if notification with same date exist, don't create a duplicate:
          return if same_date_notification.is_a?(UILocalNotification)

          notification = UILocalNotification.alloc.init
          notification.fireDate = date
          notification.alertBody = "Have you tracked your mood yet?"
          notification.soundName = UILocalNotificationDefaultSoundName
          notification.alertAction = "Track it!"

          UIApplication.sharedApplication.scheduleLocalNotification(notification)
        end
      
First we made sure that notifications can be scheduled. Then we got current date components - we will use them to create a correct date for a notification's fire date. The hours variable contains hours values when we want notifications to be fired: at 9:00AM, 14:00 (2:00PM), and 20:00 (8:00PM). When we have everything ready, we start a loop with 3.times do. This command will iterate three times, and i variable inside of it will be equal to iteration index - 0, 1, or 2. NSDate.now returns an object with a current date and time. i variable is used to create a day in the future:
        # when i == 0:
        day = NSDate.now + i.day
        # => day will be today, since today + 0.day is still today.

        # when i == 1
        day = NSDate.now + i.day
        # => day will be tomorrow, since today + 1.day is tomorrow

        # when i == 2
        day = NSDate.now + i.day
        # => day will be the day after tomorrow: today + 2.day
      
Having a correct day, we set it to the date_components hash, to create a date object from it. Then we iterate over all hour values to set :hour value to the date_components. And when we finally have complete date Hash, we pass it to create_notification method. We could add the code for notification creation to the loop, but it would make our code look heavier.

Method create_notification is used to create a notification for a given date. Because we don't want to show notification in the past, we compare current date and notification date, and we stop method execution if notification's date is in the past (notification's date is lower than current date).

Then we check already created notifications - if there is a notification that is going to fire on the same date and time as the new one, we also stop method execution because we don't want duplicated notifications to bother the user.

And then finally, we create and schedule the notification. We can set its date, text, action text that will be shown on a lock screen, sound, and badge number if needed. Then we tell the app to schedule the new notification with app's scheduleLocalNotification method.

In order to test it, you could try to set the date to the next second or so with:

        notification.fireDate =  NSDate.alloc.initWithTimeIntervalSinceNow(3)
      
and check how does it work:

The only thing left is to make the app process notifications when the app is in the foreground. There is no action the we need to do when reminder notification has been fired in the active app, so we will just print a message in the console. If you remember, method's name that is being fired when notification is received is application(app, didReceiveLocalNotification: notification). Let's add it, and post notification's info to the console:

        def application(app, didReceiveLocalNotification: notification)
          NSLog "Notification received: #{notification.alertBody}"
        end
      

Version Control

We now have notifications working, so let's make a commit:

        git add .
        git commit -m "Local Notifications added"
      

Summary

In this chapter, we learned how to send local notifications. This type of notifications can be scheduled and fired right from the app. You can use it to remind a user about some events in his calendar, or about his tasks in to-do lists.

Before sending notifications, we need to ask the user for his permission. Be polite, and tell the user why do you need it - it will increase your chances.

To schedule a notification, create a UILocalNotification object first, and then set its text, date, etc. Don't forget to pass it to the app so it will be scheduled!

More info on local notifications can be found at Apple's iOS Developer Library.

Book Index | Next