Sorting objects into an array of NSArrays

This one is pretty simple, but I had a terrible amount of difficulty working out how to do this the first time.  Cocoa makes a lot of this process pretty easy by providing the NSMutableArray class – the only real gotcha with an NSMutableArray is the “object was mutated while being enumerated” problem.  This means exactly what it says – you’ve removed an object in the array while enumerating (going through the objects one by one) the same array.  The usual workaround is to create intermediate arrays with lists of objects to delete and then enumerate over the intermediate and commit changes to your NSMutableArray.

NSArrays can only contain objects; if you want to store ints, floats etc. then you are going to need to encapsulate them in an NSObject of some sort (e.g. in this case, an NSNumber would be a suitable fit).  All of the objects in an NSArray need to be of the same class.

Say I want to make a very simple hierarchical data store which will provide the data for part of the user interface for an app; for example, a UITableView.  I want my tableView to have sections which are organised by date – you can see this sort of arrangement in, for example, email programs which sort your incoming mail by Today, Yesterday, Last Week, etc.  One simple way to do this is to establish a hierarchical structure of arrays, like so:

Master array       –> section array –> content object

–> section array (empty)

–> section array –> content object

–> content object
–> content object

This tableView is going to display a series of records, and is going to consist of multiple sections which each contain records which refer to a particular date. There are therefore two problems which need to be expressed in code; the first is to work out how many different dates there are – if I have, say, three records all of which have the same date, I want them all to be in the same section rather than putting each in its own section. The second problem is to put each record in the correct section (i.e. the correct sub-array of the main array).

Using NSMutableArrays makes reorganising your content easier to code, at the cost of some performance.  If you’re going to use a more complicated view, such as an NSOutlineView or NSBrowser, you need to set up an object class which contains an array of its own children (and, optionally, a reference to its parent), and which is able to identify whether or not it is a final leaf of the tree of references.  In this case, all of the data objects in the structure need to be of the same object class (I’ll come back to this one).

This example is assuming a model-view-controller structure (this may be initially confusing but is pretty easy to understand; you have a “controller” object which deals with your data – the “model” part – and tells your UI elements – the “view” part – how to interact with the model).  The first step is to declare your master array in the controller, as in:

@property(nonatomic, retain) NSMutableArray masterArray

in your controller’s header file and:

NSMutableArray *tempArray = [[NSMutableArray alloc]init];
masterArray = tempArray;
[tempArray release];

in your main… if you’re not using automatic reference counting (ARC) or garbage collection. If you are using either, you can basically assume that you’re not going to use any of the “release” statements here.  For those of you who are quite as new to Objective C as I was when I started coding, Cocoa has two main ways of getting your object set up – either on initialising your object or with a function which is called by your Interface Builder file, as in awakeFromNib in OSX or the various viewDidLoad functions in iOS.

Let’s assume that I have a series of NSObjects in an array, which I am going to want to sort into the sub-arrays of the masterArray.  These objects have, say, two attributes – a title held in an NSString, and a timestamp expressed as an NSDate.  I’m going to want to perform three steps, which can easily be expressed as loops and conditional statements:

  1. Enumerate over the unsorted array, to look at each object in turn. Add each unique date value to an intermediate array, giving an array of the unique dates. Optionally sort this array by date.
  2. Enumerate over array which represents the sections. (External loop)
  3. Enumerate over the source array once for each section (Internal loop). If a date hasn’t been encountered before, make a new section and file the data object in it. If a date has been encountered before, put the data object in the section which its date matches

For example (the data objects in this example are of the MyDataObject subclass of NSObject, which has the properties “myDataObjectDate” and “myDataObjectTitle”):

-(void)groupObjectsByDateFromSourceArray:(NSArray *)sourceArray intoMasterArray:(NSMutableArray *)theMasterArray {
 
     //create a temporary master array; otherwise, running this function twice will expand rather than replace the master array
     NSMutableArray *tempMasterArray = [[NSMutableArray alloc]init];
 
     int numberOfSourceObjects = [sourceArray count];
     int numberOfSections = [masterArray count];
 
     //sort the source array
     NSSortDescriptor *dateSortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"myDataObjectDate" ascending:NO];
     NSArray *sortDescriptors = [NSArray arrayWithObject: dateSortDescriptor];
     [sourceArray sortUsingDescriptors:sortDescriptors];
 
     //the first date in the source array has, by definition, not been previously encountered; hence it needs to be added
     NSMutableArray *arrayOfUniqueDates = [[NSMutableArray alloc]init];
     [arrayOfUniqueDates addObject:[[sourceArray objectAtIndex:0] myDataObjectDate]];
 
     for (int i = 1; i < numberOfSourceObjects; i ++) { 	      //get the date at index i, and the preceding date 
	 MyDataObject *firstDataObject = [sourceArray objectAtIndex:(i - 1)]; 	      
         NSDate *firstDate = [firstDataObject myDataObjectDate]; 	      
         MyDataObject *secondDataObject = [sourceArray objectAtIndex:i]; 	      
         NSDate *secondDate = [secondDataObject myDataObjectDate]; 	       	      
         //compare the two dates.  If the second date is later than the first, it has not previously been encountered and needs to be added to the list of unique dates
         if ([firstDate timeIntervalSinceDate:secondDate] > 0) {
	           [arrayOfUniqueDates addObject:secondDate];
	 }
     }
 
     int totalNumberOfSections = [arrayOfUniqueDates count];//number of sections is going to be the same as the number of unique dates
 
     //now create a new section array for each unique date and add any records with the same date to the appropriate section
 
     for (int externalLoop = 0; externalLoop < totalNumberOfSections; externalLoop ++) {
         // make a new section and add it to the temporary master array
          NSMutableArray *sectionArray = [[NSMutableArray alloc] init];
          [tempMasterArray addObject:sectionArray];
          for (int internalLoop = 0; internalLoop < numberOfSourceObjects; internalLoop ++) {
		MyDataObject *aDataObject = [sourceArray objectAtIndex:internalLoop];
	 	NSDate *theObjectDate = [aDataObject myDataObjectDate];
		NSDate *sectionDateValue = [arrayOfUniqueDates objectAtIndex: externalLoop]; 
	 	if ([theObjectDate isEqualToDate:sectionDateValue]) {
		      [sectionArray addObject:theObjectDate];
 		}
          }
     }
 
	 //finished sort; make the master array equal to the temporary master; this replaces the masterArray with the updated one
     theMasterArray = tempMasterArray;
     [tempMasterArray release];
}

If you try this out, you will probably find that it totally fails to work. Brilliant! The reason for this is that Cocoa’s isEqualToDate function checks for exact equality and NSDates have sub-millisecond precision. Hence the chance of two randomly generated dates being equal is very low. Most people get around this by setting dates as being a specific time of day – e.g. midday. This requires the use of an NSCalendar to find out exactly what NSDate is at e.g. mid-day on the day you want to represent.

NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
unsigned unitFlags = NSYearCalendarUnit | NSMonthCalendarUnit |  NSDayCalendarUnit;
NSDate *uncorrectedDate = datePicker.date;//whichever UI element you are using to enter the date; in this case a UIDatePicker
NSDateComponents *comps = [calendar components:unitFlags fromDate:uncorrectedClinicDate];
[comps setHour:12];
[comps setMinute:0];
[comps setSecond:0];
NSDate *correctedDate = [calendar dateFromComponents:comps];

 

Leave a Reply

Your email address will not be published. Required fields are marked *