I’ve been really excited about some of the new features in Leopard and the Calendar Server is a big one. To try it out, I’m writing a simple GTD-inspired task manager that interacts with the calendar server so that events are pulled and pushed dynamically to and from the server. The idea here is an app that takes just the data from iCal’s todos and puts it into a view that is more GTD friendly. The benefits are that you never have to sync and all of your information is always in iCal (thus, it works great with iPhones, Blackberries, etc). On the other hand, you are limited to storing information that iCal tasks already have (see my implementation for Projects below).
I’m using Calendars for Contexts so any calendar with begining with an @ symbol will be picked up and treated as a context. The @Inbox calendar gets a special icon, and in the future I’ll be letting you set icons for different calendars and perhaps customize the Context tolken (ie ‘@’). If you really want custom icons now, open the resource bundle. You’ll see a file named @Inbox.png. If you drop another image in there - say named @Work.png - then your @Work calendar will get the image.
The eventual plan is to make the tasks viewable as projects in an hierachal (outline) view with clever delimiters in the task names to save state in. Thus, a project for mail a package might look something like this when you are viewing it in ical
1! Mail Package
1* Get Stamps
1* Find a box
1* Go to the post office
This later feature for project is not yet implemented, and I’d love some feedback if you have a better idea of how I might structure the delimter syntax for Projects. The goals would be that it is clear and obvious, and that it displays in the right order when tasks are sorted by name in iCal (I’m not sure what most phones use as a sort ordering, but i’d be good to obey that too!).
In the meantime, I thought I’d go along a post the app and the code, which might be helpful to people trying to understand the basics of interacting with the calendar server. Keep in mind that this code is UNFINISHED, has not been well tested, and MANIPULATES ITEMS IN THE CALENDAR SERVER. It should go without saying that you need to backup your iCal files if you want to try this app. Also, it requires OS 10.5, whence the Calendar server was introduced.
[Download the App]
[Download the Source Code]
How interacting with CalCalendarStore works:
About all that Tasks Controller class does is set a sort descriptor for the tasks and do the initial grab of the calendars from the server, telling each resulting context to initialize itself using the calendar. The Task and Project controllers are currently unimplemented. The Context class is where most of the magic happens.
In Context’s initializer, you’ll notice two things. First, we add the context class as an observer of changes to its ‘theTasks’ array. Because we are using Cocoa Bindings to handle adding and removing of tasks in the UI, we need to observe changes to the array so that when they happen, we can push them to the Calendar server. More on that below.
[self addObserver:self forKeyPath:@"theTasks"
options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
context:NULL];
Additionally, we want to observe changes to the tasks themselves, which is why when we add them to theTasks, we also call addTaskAsObserver on them, which adds observers of the tasks’s ‘iscompleted’, ‘title’, and ‘datestamp’ properties.
-(void) addTaskAsObserver:(CalTask *)task {
[task addObserver:self forKeyPath:@"isCompleted"
options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
context:NULL];
[task addObserver:self forKeyPath:@"title"
options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
context:NULL];
[task addObserver:self forKeyPath:@"dateStamp"
options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
context:NULL];
}
Going the other way, we just register with the notification center to receive updates when the external calendar store changes. When that happens, we call the method updateTasks.
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateTasks
name:CalTasksChangedExternallyNotification object:[CalCalendarStore defaultCalendarStore]];
The first thing to note in the updateTasks method is that it removes itself as an observer of changes to theTasks at the begining of the block and adds itself again at the end. This is to prevent an inifinite loop of notifications that would happen if theTasks was trying to save back to the server the updates we are about to do to it.
We look at the three types of possible updates: added tasks, deleted tasks, and removing tasks. This code is based on the example code in Simple Calendar, which is a good resource for learning about the Calendar server, although the code seems a bit dated. For example, our code for adding task objects when there are new tasks on the calendar server (note that the addTask method below calls addTaskAsObserver on the new task!):
NSArray *insertedTasks = [[notification userInfo] valueForKey:CalInsertedRecordsKey];
if (insertedTasks){
for(NSString *uid in insertedTasks) {
CalTask *task = [[CalCalendarStore defaultCalendarStore] taskWithUID:uid];
if([task.calendar.title isEqualToString:self.title]) {
[self addTask:task];
}
}
}
The other big chunk of code is implementing observeValueForKeyPath, which is called when the things we observed above change (ie the user makes a change in the UI to theTasks or its tasks). Again, we need to avoid an infite loop, so it temporarily halts change notifications from the server that we are about to save to. The object that is changed and thus received by observeValueForKeyPath can be of two types: CalTask or Context - the former if the user changed a task’s properties, and the latter if they added or removed a task to theTask. We check to see which one is the case and then do the appropriate thing. For example, if the object is a CalTask, do something like:
if ([[CalCalendarStore defaultCalendarStore] saveTask:[object copy] error:&taskSavingError] == NO){
NSAlert *alertPanel = [NSAlert alertWithError:taskSavingError];
(void) [alertPanel runModal];
}
The other case is a little trickier, but it is self-explanitory and I won’t post it here.
All in all, its not too hard to interact with the Calendar Server, and I expect there will be a lot of really great apps that result from this awesome new feature in Leopard. Please post comments, questions, bug reports or ideas!
Update: A little bug fix in the code and above explination. dueDate saving was turned off and when I turned it on I was getting infinite calls of observeValueForKeyPath. Why? It turns out that saveTask does not make a copy of the object getting passed, so when the dueDate was getting validated by iCal it was causing the original object to get updated, which then got revalidated by the bindings NSDateFormatter, ad infinitum! The new code properly saves dueDates, and saves a copy of the task instead of the original to prevent the original from being edited by iCal. I’ve updated by example code above to reflect this, and posted new versions of the code and app.