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:















Sunday, October 20, 2013

Creating an inclusive list in UITableView


What I wanted to do:


I was trying to figure out a way to create a multiple selection list in UITableView where more than one row can have a checkmark. I wanted to use this as a menu that would allow the user to select categories from a list and return the selected category list to the calling view controller.
This would work like the list iOS uses in its Clock/alarm app.  

Here is a short video showing what I want to do:




video


I wanted to be able to select/deselect multiple rows. In addition to this basic functionality, I wanted to add the capability for the user to have a ‘select all’ option where they could select or deselect the entire list.

How I did it:


There are two methods that are modified to achieve this:

didSelectRowAtIndexPath - where you handle checkmarking the current cell that is selected.  Here you checkmark the cell if it isn’t marked already and take the check mark off if it is.

cellForRowAtIndexPath - where you handle the creation of the visible cells.  Here you verify whether the cell should have a checkmark or not. ( I will explain why you need this below)

In my code I have a list of category names stored in an array called categoryList and another mutable array that tracks which categories are currently checkmarked.  

self.categoryList=[[NSArray alloc]initWithObjects:@"Achievement",@"Attitude",@"Brave",@"Character",@"Courage",@"Determination",@"Enthusiasm",@"Failure",@"Fame", @"Friendship", @"Happiness",@"Inspirational",@"Leadership", @"Love", @"Motivational",@"Opportunity",@"Perseverance", @"Spiritual", nil];

self.selectedCategoryArray =[[NSMutableArray alloc]init];



I initialize these in ViewDidLoad.

Category names are displayed in a dynamic cell UITableView.

To make a simple inclusive selection list, you can follow the example on the Apple developer site which shows:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
   
   [tableView deselectRowAtIndexPath:[tableView indexPathForSelectedRow] animated:NO];
//get cell info
   UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];

   if (cell.accessoryType == UITableViewCellAccessoryNone) {
       cell.accessoryType = UITableViewCellAccessoryCheckmark;

   } else if (cell.accessoryType == UITableViewCellAccessoryCheckmark) {
       cell.accessoryType = UITableViewCellAccessoryNone;
   }

}

This code simply adds a checkmark to the current cell if it doesn’t have one already and takes the checkmark off if it does.  
If you run this code you will have the ability to check and uncheck multiple items in your list.

However you will notice a problem when you scroll to check or uncheck items that are at the bottom of your list, you might see that the checkmarks travel down, and items you did not select are checkmarked and vice versa.  This happens because cells are reused when you scroll down and the accessory information does not hold when this happens.

In order to ensure that the right cells are always checked, you need to add/remove a checkmark from the cell every time it is created by cellForRowAtIndexPath.  To do this, you need to track the selection or deselection of cells using an array, then check that array in cellForRowAtIndexPath to confirm that the cell should have a checkmark.  I will show this code after adding a few lines to didSelectRowAtIndexPath that will track this info in selectedCategoryArray:


- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
   
   [tableView deselectRowAtIndexPath:[tableView indexPathForSelectedRow] animated:NO];
   UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];

   if (cell.accessoryType == UITableViewCellAccessoryNone) {
       cell.accessoryType = UITableViewCellAccessoryCheckmark;
      //store the selected category in array
       [self.selectedCategoryArray addObject:[self.categoryList objectAtIndex:indexPath.row]];




   } else if (cell.accessoryType == UITableViewCellAccessoryCheckmark) {
       cell.accessoryType = UITableViewCellAccessoryNone;
     //remove selected category from array
       [self.selectedCategoryArray removeObject:[self.categoryList objectAtIndex:indexPath.row]];



   }

}

I add the selected cell's category name into the selectedCategoryArray or remove the category name depending on whether there is a checkmark already in the cell or not.

When the cell is created in cellForRowAtIndexPath, I make a decision about the checkmark by checking to see whether it is selected ( if the selectedCategoryArray contains the category name of the cell being created at this indexPath.row) :

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
   UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"BasicCell" forIndexPath:indexPath];
if([self.selectedCategoryArray count]) {

       if([self.selectedCategoryArray containsObject:[self.categoryList objectAtIndex:indexPath.row]]) {
      
           cell.accessoryType = UITableViewCellAccessoryCheckmark;
       } else {
           cell.accessoryType = UITableViewCellAccessoryNone;
       }
  }
   [self configureCell:cell atIndexPath:indexPath];
   
    return cell;
}



Now, I have a fully functioning inclusive list in which I can select/deselect multiple categories.

Adding “select all” capability


Here is what I want the app to do:




I want to:
1. Add an option for the user to be able to select all rows when ‘all’ is selected.  
2. When the ‘all’ is checked I want all rows checkmarked, and all checkmarks removed when 'all' is unchecked.
3. When ‘all’ is checked, and all rows are checkmarked, if at that time a row is clicked, then check marks in all other rows should be removed (including ‘all’) and only that row should contain the checkmark.

Some more logic needs to be added to both the didSelectRowAtIndexPath and CellForRowAtIndexPath to do this.

Step 1 : Add 'all' option


To add the ‘all’ option I simply added the string to the categoryList array, the dynamic UITableView takes care of displaying it:

self.categoryList=[[NSArray alloc]initWithObjects:@”all”,@"Achievement",@"Attitude",@"Brave",@"Character",@"Courage",@"Determination",@"Enthusiasm",@"Failure",@"Fame", @"Friendship", @"Happiness",@"Inspirational",@"Leadership", @"Love", @"Motivational",@"Opportunity",@"Perseverance", @"Spiritual", nil];

Step 2: 'all' checks or unchecks all rows

To add the functionality I had to add some IF statements to see if ‘all’ is selected then handle adding checkmarks and removing checkmarks from ALL rows.

Here is a function that loops through all the indexpaths in the table and add checkmarks to all rows:

-(void)checkmarkAllRows{

   //loop through all index paths and add or remove checkmark.Since we have only
   //1 section, section 0, we can have a constant for that value
   
   for(int i=0;i<[self.categoryList count];i++) {

       //create indexpath
       NSIndexPath *myIndexPath = [NSIndexPath indexPathForRow:i inSection:0];
//get cell info for that indexpath
       UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:myIndexPath];
       cell.accessoryType = UITableViewCellAccessoryCheckmark;
   }
}




To take checkmarks off:

-(void)uncheckAllRows {
   
   for(int i=0;i<[self.categoryList count];i++) {
       
       NSIndexPath *myIndexPath = [NSIndexPath indexPathForRow:i inSection:0];
       UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:myIndexPath];
       cell.accessoryType = UITableViewCellAccessoryNone;

   }
}

Here is how the didSelectRowAtIndexPath method checks if the selected category name is ‘all’ then adds/removes checkmarks from ALL rows:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
   
   [tableView deselectRowAtIndexPath:[tableView indexPathForSelectedRow] animated:NO];
   
   
   UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
   if (cell.accessoryType == UITableViewCellAccessoryNone) {
       
       cell.accessoryType = UITableViewCellAccessoryCheckmark;
       
       [self.selectedCategoryArray addObject:[self.categoryList objectAtIndex:indexPath.row]];
       
       //if 'all' row is selected then check all rows
       if([[self.categoryList objectAtIndex:indexPath.row] isEqualToString:@"all"]) {
           [self checkmarkAllRows];
       }
       
   } else if (cell.accessoryType == UITableViewCellAccessoryCheckmark) {
       
       cell.accessoryType = UITableViewCellAccessoryNone;
       
       [self.selectedCategoryArray removeObject:[self.categoryList objectAtIndex:indexPath.row]];
       
       if([[self.categoryList objectAtIndex:indexPath.row] isEqualToString:@"all"]) {
           [self uncheckAllRows];
           [self.selectedCategoryArray removeAllObjects];
           
       }         
   }

}



Remember that because of the reusing of cells, we have to account for ‘all’ in cellForRowAtIndexPath also.  In this method, I just added a condition that adds a checkmark to the cell if ‘all’ is present in the selectedCategoryArray:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
   UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"BasicCell" forIndexPath:indexPath];
   
   if([self.selectedCategoryArray count]) {
       
if([self.selectedCategoryArray containsObject:[self.categoryList objectAtIndex:indexPath.row]]
|| [self.selectedCategoryArray containsObject:@"all"]) {
      
           cell.accessoryType = UITableViewCellAccessoryCheckmark;
           
       } else {
           cell.accessoryType = UITableViewCellAccessoryNone;
        }
   }
   [self configureCell:cell atIndexPath:indexPath];
   return cell;
}

Now clicking the ‘all’ cell will check/uncheck all cells!



Step 3: Additional list behavior

I chose to make the list behave in the following way: while ‘all’ is checked, if another cell is selected, this will assume the user wants to select only that cell and so all other cells will be unchecked (Other options would be to uncheck that cell and leave the others checked so you can exclude that category, or disable selection of that cell. Depends on what kind of menu you want to create).

For this to work some more conditions need to be added, basically if a cell is selected and if ‘all’ is present in the selectedCategoryArray, then checkmarks from all cells should be removed and only the selected cell should be checked.  

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
   
   [tableView deselectRowAtIndexPath:[tableView indexPathForSelectedRow] animated:NO];
   
    UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
   if (cell.accessoryType == UITableViewCellAccessoryNone) {
       cell.accessoryType = UITableViewCellAccessoryCheckmark;
       [self.selectedCategoryArray addObject:[self.categoryList objectAtIndex:indexPath.row]];
       
       //if 'all' row is selected then check all rows
       if([[self.categoryList objectAtIndex:indexPath.row] isEqualToString:@"all"]) {
           [self checkmarkAllRows];
       }
       
   } else if (cell.accessoryType == UITableViewCellAccessoryCheckmark) {
       
       cell.accessoryType = UITableViewCellAccessoryNone;
       
       [self.selectedCategoryArray removeObject:[self.categoryList objectAtIndex:indexPath.row]];
       
       if([[self.categoryList objectAtIndex:indexPath.row] isEqualToString:@"all"]) {
           [self uncheckAllRows];
           [self.selectedCategoryArray removeAllObjects];
           
       } else if([self.selectedCategoryArray containsObject:@"all"]) {
           [self uncheckAllRows];
           [self.selectedCategoryArray removeAllObjects];
           
           cell.accessoryType = UITableViewCellAccessoryCheckmark;
           [self.selectedCategoryArray addObject:[self.categoryList objectAtIndex:indexPath.row]];

           
       }
       
   }

}



At this point, you should have a list that meets all three requirements and works like the video shown at the beginning of this post!
I can then pass the selectedCateoryArray to a calling view controller that can use the information to configure the selections in my app.


Reference

https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/TableView_iPhone/ManageSelections/ManageSelections.html#//apple_ref/doc/uid/TP40007451-CH9-SW10