Saturday, November 9, 2013

Renewing UILocalNotifications

(using iOS 7 XCode 5)



I am writing an app where I need to send the user a message at the same time everyday, indefinitely (unless the user cancels).   Alert messages can be scheduled with the OS using UILocalNotifications, for details on this check out the Apple documentation.  


It sounds simple enough: configure a local notification to appear every day at a time of the user’s choosing.  I could use the repeatInterval property which allows the app to schedule a daily alert.  But I want the user to receive a different message everyday and the repeatInterval property will show the same message body everyday.


So I have to schedule each alert separately each with a different message, and time.  Also note that there is a maximum number of notifications that can be scheduled at a given time (64 according to documentation).  So in order to schedule notifications that span over months either the app has to automatically keep adding notifications at a later date or the user has to manually reschedule them after 64 days.  I prefer the first solution where the app figures out that an alert has been shown and adds a new notification to the queue.  But for this to happen I would need a background process running even when app is not in the foreground.  According to my Googling, this is not allowed by the OS (to be certain I asked stackoverflow).  The second option seems cumbersome for the user.

My solution includes both of the options, having the user initiate the rescheduling by opening the app from the alert and having the app automatically reschedule when it is launched.  When an alert is shown, the action button says ‘go to app’ and it launches the app.  This ‘go to app’ action will take the user to the main page of the app, in the background, the app figures out how many local notification slots are available and then adds more notifications to the schedule.  This solution needs the user to click one button, and the app does the rest.  (When shown in a locked screen the user just needs to slide the alert and is taken to the app, again the app reschedules at that point.)  


I will break this up into two parts:
1. scheduling daily alerts (64 total)
2. adding to schedule in the background when app is launched.


Scheduling daily alerts



In my app I used a time picker to let the user choose the time of day they would like to receive their daily message and had them determine the content.  However for the sake of this example, let’s assume that the current time is used to schedule the notifications, and the alert messages are contained in an array of strings called userDefinedMessages.


Here is how I configure a local notification that repeats every day with a different alert message for 64 days:


//Create a calendar
NSCalendar *currentCalendar = [NSCalendar currentCalendar];


//Create a day component offset, days will be incremented by 1
NSDateComponents *dayComponent = [[NSDateComponents alloc] init] ;
[dayComponent setDay:1];


//Create date from current date so notifications start being scheduled from current time details here
NSDate *startDate =[NSDate date];


//Configure 64 local notifications, increment day component every time
for(int i =0;i<64;i++) {


UILocalNotification *localNotif = [[UILocalNotification alloc] init];
localNotif.timeZone = [NSTimeZone defaultTimeZone];
//content of the alert message are set from an array of messages set by user, not shown here
localNotif.alertBody = [userDefinedMessages objectAtIndex:i];
localNotif.alertAction = @”Go to App”;
//set the alert schedule date
startDate = [currentCalendar dateByAddingComponents:dayComponent toDate:startDate options:0];


localNotif.fireDate = startDate;
localNotif.soundName = UILocalNotificationDefaultSoundName;


[[UIApplication sharedApplication] scheduleLocalNotification:localNotif];


}
                   

Renewing Local Notifications



When a local notification fires, whether the alert launches the app or the app is already in the 
foreground, the didReceiveLocalNotification method will be called.  This can be added to the 
AppDelegate.m file.  In my case, when the app is launched from an alert, I check to see how 
many local notifications are left in the queue, if there is room, I add more.



- (void)application:(UIApplication *)application
didReceiveLocalNotification:(UILocalNotification *)notification {


//Get count of currently scheduled notifications, details here


int numberOfScheduledLocalNotifications = [[[UIApplication sharedApplication] scheduledLocalNotifications] count];
   
   int numberOfEmptyNotificationSlots = 64 - numberOfScheduledLocalNotifications;



//Sort the scheduledLocalNotifications array by firedate so that the last notification is
// first in the array.   This way we can find the last notification scheduled and add new notifications after that date. 
//This sorting code was obtained from here.


NSSortDescriptor *sortDes = [NSSortDescriptor sortDescriptorWithKey:@"fireDate" ascending:NO];

NSArray *notifArray = [[[UIApplication sharedApplication] scheduledLocalNotifications] sortedArrayUsingDescriptors:@[sortDes]];
   

//Get the date alert is scheduled for from the local notifications if there are any scheduled, otherwise just use 
//current date

   NSDate *fireDate;
   
   if([notifArray count]) {
       UILocalNotification *lastNotif=  [notifArray objectAtIndex:0];
       fireDate = lastNotif.fireDate;
   } else {
       fireDate=[NSDate date];
   }


//Create a calendar
   NSCalendar *currentCalendar = [NSCalendar currentCalendar];
  
   //Create a day component offset, days will be incremented by 1
   NSDateComponents *dayComponent = [[NSDateComponents alloc] init] ;
   [dayComponent setDay:1];
   
  //Configure local notifications, increment day component every time
for(int i =0;i<numberOfEmptyNotifiationSlots;i++) {


UILocalNotification *localNotif = [[UILocalNotification alloc] init];
localNotif.timeZone = [NSTimeZone defaultTimeZone];


//content of the alert message are set from an array of messages set by user, this array can be saved in core
//data then retrieved here or saved in NSUserDefaults
localNotif.alertBody = <get from user array>;


localNotif.alertAction = @”Go to App”;
//set the alert schedule date to be a day after the last scheduled notification
       fireDate = [currentCalendar dateByAddingComponents:dayComponent toDate:fireDate options:0];
       
       localNotif.fireDate = fireDate;
       localNotif.soundName = UILocalNotificationDefaultSoundName;
       
       [[UIApplication sharedApplication] scheduleLocalNotification:localNotif];
}
}
This will add more notifications to the schedule when the user launches the app from the alert 
message.  


Please note that there is a way to schedule notifications using the scheduledLocalNotification 
which I have not used here.
References:















3 comments:

  1. Hi

    First off great work putting this solution together.

    Not sure if my comment went through, but I'll type it again. I saw your question on stackoverflow which led me here. I was looking at implementing a similar randomizer for location notifications

    Look at your solution, it's exactly as expected except for one thing. Initially you create 64 notifications to fill your available slots but you don't set your dayComponent to increment. Which in my option would result in them all being scheduled for the same time.

    I see you that you do increment your dayComponent when you renew them though. I was wondering if that's just a typo or intended? Also what format is your userDefinedMessgaes? Is it an array or registered into your ,plist as a dictionary?

    Cheers
    Ace

    ReplyDelete
    Replies
    1. Looking at your "Renewing Local Notifications" section, I see that you have the intention of incrementing your dayComponent but don't actually do it. Am I missing something?

      Delete
    2. Hi Ace,
      Thanks for looking through my solution. In my code I schedule the local notifications within a FOR loop, and I also use :
      startDate = [currentCalendar dateByAddingComponents:dayComponent toDate:startDate options:0];

      this will add a day to the startDate at every iteration of the loop. I hope this answers your question?
      Also the userdefinedmessages are stored as an array.

      thanks!
      Shveta

      Delete