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:






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

Friday, September 27, 2013

Data Persistence using PLIST



Data Persistence using PLIST


I was working on an app a few months ago in which I needed data persistence.  I hadn’t yet mastered Core Data so I found that I could use property lists to store the data.  After some reading and lots of google searches, I found that the following solution worked for me.
This post assumes you already know your way around Xcode and basic iOS programming.  This was programmed in Xcode version 4.6 for iOS5.

The Data



Here is what the data looks like in the UITableViews:

   

The UITableView contains restaurants, corresponding dishes and ratings  I decided to store the data in the following format:


I made a NSMutableArray of NSMutableDictionaries which contains the restaurant name and a NSMutableArray of dishes from the restaurant.  This dish array is made up of NSMutableDictionaries that contain the dish name and dish rating. This diagram might help clarify:


Plists can store data in arrays and dictionaries, so in this way I did not have to convert the data format.  I simply included a plist file in the app’s Supporting Files directory, then in ViewDidLoad of my master view controller I copied the contents of the file into an array.  Then modified the dictionary values within the array when the user added or deleted an item and saved it back into the file when the app exit.

Creating Plist file



To create a plist file in XCode, go to File->New->Resources->Plist, save the file in the supporting files directory.  You can then modify the plist file and add the data you would like to use in your app.  In my case the plist file looked something like this:



Loading Plist values



With the .plist file under the supporting files folder, I read in the file in the MasterViewController file in the viewDidLoad method.


//search documents directory
NSString *rootPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, YES) objectAtIndex:0];
   
plistPath = [rootPath stringByAppendingPathComponent:@"restaurants.plist"];


//check resource directory
if (![[NSFileManager defaultManager] fileExistsAtPath:plistPath]) {
       plistPath = [[NSBundle mainBundle] pathForResource:@"restaurants" ofType:@"plist"];
   }
//load array
self.restaurantArray = [[NSMutableArray alloc] initWithContentsOfFile:plistPath];


(Code referenced from here)


Saving Plist values



After adding data to my restaurant and dish dictionaries, I call this function to save the data into the documents folder.


   NSString *rootPath1 = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
   NSString *plistPath1 = [rootPath1 stringByAppendingPathComponent:@"restaurants.plist"];
   
   [self.restaurantArray writeToFile:plistPath1 atomically:YES];


(Code referenced from here)


With this I was able to load, add, modify and save data to my plist.  Modifying the data consisted of adding objects into arrays and dictionaries where needed.
Of course there are other ways of doing this, but I found this way worked for me till the next release of the app in which I used Core Data instead.


Reference Links





Plist reference


Code reference