Thursday, December 31, 2009

Happy New Year! Closing Out 2009

Well, Happy New Year!

Thus ends (almost) my first full calendar year working exclusively with Apple technologies and also my first full calendar year as a published book author. I started working on Beginning iPhone Development in March of 2008, but because of the NDA and very beta-status of the early SDK releases, didn't start full-time writing on it until after WWDC 2008.

It's been a hell of a year. We finished the year on a positive note: More iPhone 3 Development shipped out a few days ago and some people have received it already. I've also heard that Beginning iPhone Development is fairly close to breaking 100,000 copies sold between the two editions. It's hard to say for sure if that has happened because of the way books are inventoried and sold, but our sell-through (books known to be in the hands of consumers) was over 75,000 several months ago. When you consider that Apress' expectation for our book when we contracted with them was that it would probably sell between 4,000 and 5,000 copies, that's really something. We were told before the book went to press that Apress would consider it huge success if it sold 10,000 copies.

I have to say that I love what I'm doing now much more than anything else I've ever done for a living, and infinitely more than what I was doing when I started writing the first book. I'm really glad I've been able to do this. I used to travel 48 or 50 weeks a year doing large-scale Enterprise software implementations. That was a realm where most of the challenges came not from the complexity of the algorithms (most of the work was pretty easy stuff), but the complexity of the social landscape and the politics of working in a large organization. As regular readers know, I'm not shy about giving my opinion even when it's unpopular, so you can imagine what I was like in highly-politically charged corporate environments. Yeah. I was miserable, even though I was good at it and it paid well. But the schedule was brutal. I'd leave my house like 2:00 or 3:00 am Monday morning or sometimes even Sunday night, and I'd get back late Friday night every week. I lived in hotel rooms and ate in hotel bars, and missed most of my kids' birthdays and other special events.

This is so much better. In many ways, this is the life. But, there's a downside to it as well, one that I've been aware of for a while but am only now coming fully to grips with: Even a "best selling" programming book doesn't really put the money on the table the way consulting does, and the blog doesn't generate any income worth mentioning. Some year-end financial calculations have made me realize that I've got some tough decisions to make about how I spend my time in 2010.

Books

At this point, I'm going to go out on a limb and say that I won't be writing any books in 2010. I really enjoy writing books, and have a couple knocking around in my head that I'd love to get out on paper, including a soup-to-nuts OpenGL ES book, but I really need to focus on projects that bring in money more effectively and more immediately (royalties take no less than six-months, and often longer to start rolling in).

Dave and I both have similar philosophies about writing books, and neither of us are willing to rush a book out just to capitalize on a trend or hot market. But that, combined with the way we work, means writing books is incredibly time-intensive for us and there's no way to avoid that without compromising what we believe. I'm not comfortable writing about something I don't fully understand, and it simply takes time to wrap my head around something well enough to explain it to others.

That being said, if someone came along and offered a huge advance for a book I wanted to write, I could be tempted. But given the reality of the tech book market, the chances of somebody offering the kind of money it would take to make it worthwhile are slim at best.

If 2010 goes well for me financially, though, I'd like to write another book in 2011.

The Blog

I have no intention of stopping work on my blog. I enjoy it, and some of you seem to as well. I will be more focused in 2010 and will mostly stick to technical subjects and covering things directly relevant to Mac and iPhone programmers. Writing lengthy opinion pieces is time-consuming and tends to provoke people in ways that I don't necessarily intend, even if I do get a lot of page hits from them. But I definitely do plan to continue posting installments of the OpenGL ES from the Ground Up as well as more virtual chapters for our existing books and code snippets that I write that might be useful to others.

The pace of new installments may not be as frequent as it was this past year, at least the part of last year when I wasn't actively working on a book, but the posts will definitely continue.

Teaching

I'm still thinking about whether I want to continue teaching workshops. It's decent money, and I rather enjoy it, but I'm not sure I want to get into doing regular travel again and I don't want the workshops to interfere with my ability to take on contracting work. I'll probably continue to teach, but probably not more than every other month or so. We'll see, though.

Conferences

I'm planning on doing a handful of conferences this year. I'm speaking at both NSConferences in 2010 and, of course, plan to attend WWDC assuming I can scrape together the money. I'd really, really hate to miss WWDC - it's my absolute favoritest week of the year - but unfortunately, it's not a sure thing at the moment. I've also been accepted as a speaker at 360|iDev San Jose.

Contracting

This is what I really need to focus on for the next year. It is my intention to stay working within the Mac and iPhone space if at all possible, so if you know of anybody looking to contract or subcontract an experienced iPhone or Mac developer, please do feel free to send them my way. Though I'm willing to travel occasionally, I am not interested in contracts that would require full-time travel or anything close to it. I'm also available for corporate training or reviewing code or application architecture. I'm a very experienced troubleshooter and have spent a lot of time investigating how to architect iPhone applications, so if you've got projects that are running into problems, I may be able to help get you back on track. As a general rule, I'm not currently looking for full-time employment, though there's a small handful of jobs (primarily at Apple) that I'd definitely consider if they opened up.

If you want to reach me about anything, you can send e-mail to jeff underscore lamarche at mac dot com.

So, on that note, I'm going to go enjoy my New Year's even and forget about anything to do with finances or the iPhone for a few hours. I wish you all a prosperous and enjoyable 2010, and I'll be back to posting in a few days.



MDN Community Awards

Today, somebody let me now that I've been nominated for the MDN Community Awards 2009. This came as quite a shock. I'm not sure who nominated me, but thank you. It's incredibly flattering to see my name listed alongside the names of so many people that I admire and respect.

I really don't envy the people at MDN who have to make the final decision on this. There are some really awesome people on that list, and I expect several more names to be on the list before all is said and done. It's not going to be an easy list to narrow down.



Wednesday, December 30, 2009

Updated Navigation-Based Core Data Application Template

Periodically, while working on the Core Data chapters in More iPhone 3 Development, I would update my copy of Apple's provided Navigation-Based Core Data Application project template with logic to handle different common scenarios. While working on the previous blog post, I made some more tweaks to the template to better handle object changes that trigger managed objects to move between sections when specifying a sectionNameKeyPath. You can find the most recent version of this project template here, and I've included the four delegate methods from the template at the end of this blog post.

I've really tried to give NSFetchedResultsController the benefit of the doubt. It's a really, really great concept and when it works, I really like it, but the execution is way below Apple's normal level of code quality. All of the code I've added to this template is needed simply to handle typical use-cases supposedly supported by this class… things that should be handled out of the box without any need for additional code. In this particular case, the delegate methods should really only be required for handling extraordinary or unusual scenarios.

If I have time, I may try to encapsulate this logic into a subclass of NSFetchedResultsController, but for the time being, if you use this template or the delegate methods at the end of this posting, it should take care of the bulk of situations for you automatically. I suspect that there will be a few more tweaks to this code as I use it in real-world code, but if there are, I'll post the changes and a new version of the template here. In any case, this template is a far better starting point than Apple's provided template and may just keep you from pulling all your hair out.

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
[self.tableView beginUpdates];
}

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self.tableView endUpdates];
}

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
switch(type) {
case NSFetchedResultsChangeInsert:
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate: {
NSString *sectionKeyPath = [controller sectionNameKeyPath];
if (sectionKeyPath == nil)
break;
NSManagedObject *changedObject = [controller objectAtIndexPath:indexPath];
NSArray *keyParts = [sectionKeyPath componentsSeparatedByString:@"."];
id currentKeyValue = [changedObject valueForKeyPath:sectionKeyPath];
for (int i = 0; i < [keyParts count] - 1; i++) {
NSString *onePart = [keyParts objectAtIndex:i];
changedObject = [changedObject valueForKey:onePart];
}

sectionKeyPath = [keyParts lastObject];
NSDictionary *committedValues = [changedObject committedValuesForKeys:nil];

if ([[committedValues valueForKeyPath:sectionKeyPath] isEqual:currentKeyValue])
break;

NSUInteger tableSectionCount = [self.tableView numberOfSections];
NSUInteger frcSectionCount = [[controller sections] count];
if (tableSectionCount != frcSectionCount) {
// Need to insert a section
NSArray *sections = controller.sections;
NSInteger newSectionLocation = -1;
for (id oneSection in sections) {
NSString *sectionName = [oneSection name];
if ([currentKeyValue isEqual:sectionName]) {
newSectionLocation = [sections indexOfObject:oneSection];
break;
}

}

if (newSectionLocation == -1)
return; // uh oh

if (!((newSectionLocation == 0) && (tableSectionCount == 1) && ([self.tableView numberOfRowsInSection:0] == 0)))
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:newSectionLocation] withRowAnimation:UITableViewRowAnimationFade];
NSUInteger indices[2] = {newSectionLocation, 0};
newIndexPath = [[[NSIndexPath alloc] initWithIndexes:indices length:2] autorelease];
}

}

case NSFetchedResultsChangeMove:
if (newIndexPath != nil) {

NSUInteger tableSectionCount = [self.tableView numberOfSections];
NSUInteger frcSectionCount = [[controller sections] count];
if (frcSectionCount >= tableSectionCount)
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:[newIndexPath section]] withRowAnimation:UITableViewRowAnimationNone];
else
if (tableSectionCount > 1)
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:[indexPath section]] withRowAnimation:UITableViewRowAnimationNone];


[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[self.tableView insertRowsAtIndexPaths: [NSArray arrayWithObject:newIndexPath]
withRowAnimation: UITableViewRowAnimationRight
]
;

}

else {
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:[indexPath section]] withRowAnimation:UITableViewRowAnimationFade];
}

break;
default:
break;
}

}

- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
switch(type) {

case NSFetchedResultsChangeInsert:
if (!((sectionIndex == 0) && ([self.tableView numberOfSections] == 1)))
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
if (!((sectionIndex == 0) && ([self.tableView numberOfSections] == 1) ))
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeMove:
break;
case NSFetchedResultsChangeUpdate:
break;
default:
break;
}

}




Tuesday, December 29, 2009

SuperDB Core Data App with Sections

If you've gotten through the first few chapters of More iPhone 3 Development, you might be wondering why we included a sectionNameKeyPath when we didn't actually divide up the table into sections. What's the point of having the fetched results controller use sections if they're not displayed?

The truth of the matter is that we originally planned to take the SuperDB application further than we were able to. Unfortunately, we reached a point where we had to cut it off and move on to other topics in order to both meet our deadline and to come in at a reasonable page count (as it was, we came in 250 pages over what we contracted for). Okay, we didn't actually meet our deadline, but we would have missed it by more.

Dave and I agreed to stop working on Core Data to get the book done and leave room for the other topics, but we left open the possibility of expanding the application further here in my blog. In order to be able to do that, we left in some vestiges of the original plan to make it easier to expand the application here.

Here's the first expansion, which is to add alphabetical sections to the table, like so:

Screen shot 2009-12-29 at 5.14.44 PM.png


Let's continue on from the code in the 07 - SuperDB project. You can download the revised version from here. Make a new copy of it if you wish. The first thing we need to do is add a tableview delegate method that returns the title to be displayed in each section. To do that, add the following code to HeroListViewController.m, near the other table view methods:

- (NSString *)tableView:(UITableView *)tableView 
titleForHeaderInSection:(NSInteger)section {

if (!(section == 0 && [self.tableView numberOfSections] == 1)) {
id <NSFetchedResultsSectionInfo> sectionInfo =
[[self.fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo name];
}

return nil;
}

This is a very generic method that will use the values from the fetched results controller. This method becomes basically a copy-and-paste bit of code that you can use unchanged in any controller that uses a fetched results controller with sections.

If you run your application now, however, you're going to get a separate section for each row (it would also crash, but we'll deal with that as well). In the version you've got now, we specified either name or secretIdentity as our sectionNameKeyPath, so every unique name or secret identity becomes its own section. Generally, not what want. So, the next step is to add virtual accessor method to our Hero object to return the value that we want to use to use to divide the list of heroes up. Let's do it alphabetically, so that means we need to create methods to return the first letter of the name and the secret identity. We can then use these new virtual accessor methods as our section name key paths, and the fetched results controller will divvy up the list by first letter.

In Hero.h, add the following two method declarations, just before the @end keyword:

- (NSString *)nameFirstLetter;
- (NSString *)secretIdentityFirstLetter;

Save Hero.h and switch over to Hero.m, and insert the implementation of these two methods, right above the @end keyword again:

- (NSString *)nameFirstLetter {
return [self.name substringToIndex:1];
}

- (NSString *)secretIdentityFirstLetter {
return [self.secretIdentity substringToIndex:1];
}

Save Hero.m. Next, we have to make a few changes in HeroListViewController.m. First, change the assignment of sectionKey to reflect our new virtual accessor methods. Look for the following code in the fetchedResultsController method and change the lines in bold to switch from using name and secretIdentity to using our new first-letter methods:

...
case kByName: {
NSSortDescriptor *sortDescriptor1 = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES];
NSSortDescriptor *sortDescriptor2 = [[NSSortDescriptor alloc] initWithKey:@"secretIdentity" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor1, sortDescriptor2, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
[sortDescriptor1 release];
[sortDescriptor2 release];
[sortDescriptors release];
sectionKey = @"nameFirstLetter";
break;
}

case kBySecretIdentity:{
NSSortDescriptor *sortDescriptor1 = [[NSSortDescriptor alloc] initWithKey:@"secretIdentity" ascending:YES];
NSSortDescriptor *sortDescriptor2 = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor1, sortDescriptor2, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
[sortDescriptor1 release];
[sortDescriptor2 release];
[sortDescriptors release];
sectionKey = @"secretIdentityFirstLetter";
break;
}

...


That's basically it. Well, no really. In testing this, I found that it crashed. For this new version, we need to tweak the fetched results controller delegate methods that we gave you in Chapter 2. Replace your existing implementations of controller:didChangeSection:atIndex:forChangeType: and controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: with the following new versions:

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
switch(type) {
case NSFetchedResultsChangeInsert:
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate: {
NSString *sectionKeyPath = [controller sectionNameKeyPath];
if (sectionKeyPath == nil)
break;
NSManagedObject *changedObject = [controller objectAtIndexPath:indexPath];
NSArray *keyParts = [sectionKeyPath componentsSeparatedByString:@"."];
id currentKeyValue = [changedObject valueForKeyPath:sectionKeyPath];
for (int i = 0; i < [keyParts count] - 1; i++) {
NSString *onePart = [keyParts objectAtIndex:i];
changedObject = [changedObject valueForKey:onePart];
}

sectionKeyPath = [keyParts lastObject];
NSDictionary *committedValues = [changedObject committedValuesForKeys:nil];

if ([[committedValues valueForKeyPath:sectionKeyPath] isEqual:currentKeyValue])
break;

NSUInteger tableSectionCount = [self.tableView numberOfSections];
NSUInteger frcSectionCount = [[controller sections] count];
if (tableSectionCount != frcSectionCount) {
// Need to insert a section
NSArray *sections = controller.sections;
NSInteger newSectionLocation = -1;
for (id oneSection in sections) {
NSString *sectionName = [oneSection name];
if ([currentKeyValue isEqual:sectionName]) {
newSectionLocation = [sections indexOfObject:oneSection];
break;
}

}

if (newSectionLocation == -1)
return; // uh oh

if (!((newSectionLocation == 0) && (tableSectionCount == 1) && ([self.tableView numberOfRowsInSection:0] == 0)))
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:newSectionLocation] withRowAnimation:UITableViewRowAnimationFade];
NSUInteger indices[2] = {newSectionLocation, 0};
newIndexPath = [[[NSIndexPath alloc] initWithIndexes:indices length:2] autorelease];
}

}

case NSFetchedResultsChangeMove:
if (newIndexPath != nil) {

NSUInteger tableSectionCount = [self.tableView numberOfSections];
NSUInteger frcSectionCount = [[controller sections] count];
if (frcSectionCount >= tableSectionCount)
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:[newIndexPath section]] withRowAnimation:UITableViewRowAnimationNone];
else
if (tableSectionCount > 1)
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:[indexPath section]] withRowAnimation:UITableViewRowAnimationNone];


[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[self.tableView insertRowsAtIndexPaths: [NSArray arrayWithObject:newIndexPath]
withRowAnimation: UITableViewRowAnimationRight
]
;

}

else {
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:[indexPath section]] withRowAnimation:UITableViewRowAnimationFade];
}

break;
default:
break;
}

}

- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
switch(type) {

case NSFetchedResultsChangeInsert:
if (!((sectionIndex == 0) && ([self.tableView numberOfSections] == 1)))
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
if (!((sectionIndex == 0) && ([self.tableView numberOfSections] == 1) ))
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeMove:
break;
case NSFetchedResultsChangeUpdate:
break;
default:
break;
}

}

Since our original application gave every row its own section, we didn't have this problem, but what happens is when we receive notice that a row has moved, we need to make sure that the number of sections in the table and controller match. It's possible that a row moved to a new section, possibly even causing a row to be deleted or inserted in the controller, but not the table. If a row is moved from one section to another, and we don't account for it, the app will crash. If the moved row had been the last row in a section, we need to delete the section from the table, unless it was the last section, because tables have to have at least one section. If moving the row created a new section in the controller, we insert a new row into the table.

And that's really it, you now have your SuperDB's hero list divide up by first letter of either the hero's name or secret identity, depending on which tab was selected.

You should use the fetched results controller delegate methods from this posting instead of the ones in the book. The ones in the book work just fine for the application's in the book, but this one is more flexible and will handle a greater variety of situations correctly.