Calendar Controls on iOS

May 01, 2011

When developing GrowJo I investigated two open source calendar solutions for iOS; Tapku and Kal. The Tapku library includes way more than just a calendar but I elected to use a stripped down version of the library with only the components needed– Tapku can do this because of its modular design. I really liked the code behind Tapku more and it fit nicely into the existing Cocoa setup, especially the month table view. I dislike the method names for the Kal API. However, I ended up going with Kal because it was lightweight and just happened to be the solution I discovered first.

This post describes my approach at using a core data backing store to manage which days have markers or tags associated with them. For the calendar, this is how each date gets an indicator and how the underlying table view is populated when you select a marked date.

I decided against using a fetched results controller. I didn’t seem to make sense for my scenario. When I add tags to my calendar I can do by presenting a model view so I do not need to animate in the entry of new tags. I don’t support deleting a tags.

I subclassed KalViewController and created a private object implementing the KalDataSource protocol to act as my data source. I extended the KalDataSourceCallbacks protocol with a method to place the markers from core date and callback to my KalViewController when complete. The method loadTagsForDelegate: is called from my KalViewController when the view is ready to display marks upon the calendar. Since I am retrieving tags directly from core data, I don’t really need to do anything in this method. I can just inform the delegate that the data source has been loaded.

- (void)loadTagsForDelegate:(id<KalDataSourceCallbacks>)delegate {
  // just a dummy method since we pull tags directly from core data
  [delegate loadedDataSource: self];
}

As the user moves from month the month there is no need to refetch all the tags from the database again– I can just filter out relevant tags for the set of visible days. I should only have to reload tags from the database when a new tag is saved or the view is initially displayed. The action happens in the method tagsFrom:to: which returns an NSArray of tags between two dates. This method is called each time I need the tags for the current month. As presentingDatesFrom:to:delegate: is called as the months move forwards backwards, I make a dummy call to load new tags since tags are coming directly from care data. Here is how my data source conforms to the KalDataSource protocol:

- (void)presentingDatesFrom:(NSDate *)fromDate to:(NSDate *)toDate delegate:(id<KalDataSourceCallback>)delegate {
  // called as we move from the month the month
  [self loadTagsForDelegate:delegate];
}

- (NSArray *)markedDatesFrom:(NSDate *)fromDate to:(NSDate *)toDate {
  // returns an NSArray of tags between fromDate and toDate
  // this is called for a range of visible date cells (e.g. approx. one month)
  return [[self tagsFrom:fromDate to:toDate] valueForKeyPath:@"date"];
}

- (void)loadItemsFromDate:(NSDate *)fromDate toDate:(NSDate *)toDate {
  // load all the tags from beginning to end of date
  [self removeAllItems];
  [self.items addObjectsFromArray:[self tagsFrom:fromDate to:toDate]];
}

- (NSArray *)tagsFrom:(NSDate *)fromDate to:(NSDate *)toDate {
  // filter tags based on a date range
  NSMutableArray *matches = [NSMutableArray array];
  for (Tag *tag in self.plant.tags)
    if ([tag.date isBetweenDate:fromDate andDate:toDate])
      [matches addObject:tag];
  
  return matches;
}

My KalDataSource also matches the underlying table view with the following methods.

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
  // the items array is reload as each day is selected.
  // items is a list of the markers for each day
  return [items count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {    
  // standard code to dequeue or initialize a new cell
  Tag *tag = [self tagAtIndexPath:indexPath];
  // configure and return the cell
}

When I want to add a new tag, I do the typical insert via core data and then post a KalDataSourceChangedNotification notification that the data source has changed. This will trigger to Kal framework to reload the tags.

- (void)addTagWithType:(NSString *)type {
  // save a new tag with the type given and selected tag in kal
  Tag *aTag = [NSEntityDescription insertNewObjectForEntityForName:@"Tag" inManagedObjectContext:context];

  aTag.type = type;
  aTag.date = self.selectedDate;
  aTag.plant = plant_;

  NSError *error;
  if (![context save:&error]) {
    NSLog(@"Whoops, couldn't save: %@", [error localizedDescription]);
  }

  // and after receiving a below notification our delegate
  // will request fresh tags from the database
  [[NSNotificationCenter defaultCenter] postNotification:
   [NSNotification notificationWithName:@"KalDataSourceChangedNotification" object:nil]];
}

Update: I’ve since found a few really good alternatives to the controls mentioned above. If I was going to reimplement this today, I would definately consider using TimesSqaure, ABCalendarPicker, or CKCalendar.