Thursday, January 15, 2009

iTimeZone 1.1 Post-Mortem, or Limiting Features Based On How Little You Know

iTimeZone 1.2 is out, so I figured it was finally time to do a post-mortem on iTimeZone 1.1.

Picking Your Battles
One of the challenges when trying to ship a product using a new development framework is limiting the amount of stuff you need to know to implement your features. This is particularly acute for indie developers since they are either a team of one or few. Dividing up feature assignments according to unknown frameworks isn't a viable tactic. Instead, shipping features have to be limited to minimize the frameworks the developer(s) must learn. That reasoning led to iTimeZone 1.0's city list being extracted from the built-in iPhone OS time zone data. There was too much engineering to make a large city list work for someone (aka me) that had never done any real OS X, Objective-C or Cocoa development and make a July iTunes App Store release in their spare time. It was obvious that a large city list had to be in iTimeZone 1.1, the question was just how to implement it.

Missed Wide Right
There where two possible paths, and the frameworks those approaches mandated, to get cities:

  • Web Service:
    • Threading. Every character a user types starts a new search. In this case, a new web service call
    • XML Parsing. Going to have to crack the results and create objects
    • Caching. Should cut down on web service queries by storing user selected cities and search results locally, either in memory or on disk
  • Local SQLite Database
    • Threading. Every character a user types starts a new search. In this case, a new DB query
    • SQLite. Needed to store databases locally and execute queries
    • Caching. If queries are expensive, cache results in memory

Deciding which engineering approach to use largely depended on the data source for the large city list, and how did they provide the data. I found GeoNames, which not only had a web service but an export of their city list in CSV format. That didn't help, GeoNames was too flexible, it was completely in my court.

I decided to go for using a Web Service first for these reasons:

  1. The app and others are going to need all kinds of data from the Web in the future, this is the most reusable investment
  2. Everyone uses web services, it has to be fast enough
  3. Every iPhone has permanent connectivity, and iPod touch has WiFi, requiring a network connection won't be a big deal
  4. I didn't want to worry about stale data.

It turns out I was wrong on all four points above! Here's why in order:

  1. I was only partially wrong on this. Yes, iTimeZone or other apps are going to have to pull data from the Web, and I figured out how to do this in Cocoa, but I wasn't going to be using it because...
  2. Geonames.org's Web Service's (at least the free one) performance was too unpredictable. I could be in a full strength 3G signal, and wouldn't receive first results from the service for many seconds after tapping a character. It worked eventually usually, but I never felt comfortable with it because I couldn't guarantee what end users would experience. That is kinda endemic to Web connected apps to some degree, and it will be acceptable for a lot of data, I wasn't happy with it for iTimeZone
  3. iTimeZone has to work without a network connection. I don't know why I ever thought it could require a network connection. We here at Tangerine Element use it in places that are likely to have limited or no connectivity, and getting on the WiFi outside of known hotspots turns out to be an exercise in frustration.
  4. City and time zone data is relatively slow changing. Sure, city names change and governments change daylight savings rules all the time. I realized though that I could simply continue using the iPhone's built-in time zone data, the offsets and effective dates, matched up based on the time zone id.
I sunk over a month building a prototype to pull city data from Geoname.org, learning threading to make it more responsive, and figuring out how I was going to cache the data, but at some point performance wasn't predictable enough and network connectivity so spotty were I was testing, I bit the bullet and gave up on it.

So You Want To Use a Database on the iPhone...
Most of what I had learned about threading and caching large results sets because of the GeoNames prototype was applicable when using SQLite. Using SQLite though had it's own set of challenges.

  • SQLite is a C, not Cocoa, library. This meant understanding how to convert between Objective-C data types and what SQLite expected. Mostly easy, with the exception of how to format LIKE queries.
  • There are a bunch of calls and interaction patterns to get right. I mostly used the SQLiteBooks sample application Apple provides to iPhone SDK members to understand this, but there were a few things I learned from the APRESS book The Definitive Guide to SQLite. I confess though I haven't read it cover to cover, just spot read where I needed to learn something.
  • Mac OS X only comes with command-line tools to create SQLite database. So I bought SQLabs SQLiteManager for $49. Easily worth it for the ability to easily import CSV files into database tables.
This all had to be done largely because CoreData doesn't exist on the iPhone OS 2.2 and below. Eventually though by importing the GeoNames cities CSV file into a SQLite database, and some massaging of the data, I had a pretty good city list.

Lessons Learned About iPhone Development
What did I learn going while developing iTimeZone 1.1?

  • The SQLiteBooks example is not the ideal pattern. This is perhaps pedantic, but I should have just created a SQLiteController and put everything in there. I haven't done this yet even in iTimeZone 1.2, but when I feel like it, I'll probably move stuff around so that if CoreData drops in a future OS release, my storage code is already disentangled from AppDelegate.
  • Preparing SQLite statements is time consuming. I ended up putting the preparation of the most commonly used statements in another thread at launch, the delay doing them inline was pretty high the first time, and there was very little reason to make the user what when a thread could do the work in the background.
  • Consider using a third-party library. The one I know of is Gus Mueller @ Flying Meat's FMDB. I choose not to use it because I was nervous about committing to it only to find it didn't do something I wanted and having to add it myself, but this is another decision I might reevaluate depending on the CoreData situation.
  • Creating threads might be expensive, reuse them. I still haven't done this in iTimeZone 1.2, I am creating and destroying threads for every character typed into search, but that is probably not a high enough usage to justify the rewrite at this point since it performs well enough.
  • Use the Leaks Performance Tool. No matter what, you are going to leak memory, make sure you are using this tool to find and stop memory leaks.
  • Do as much on background threads as you can. Pretty much since device OSes have had threading this rule stands, but just to reaffirm it's truth. My examples are preparing SQLite statements and loading the top X number of cities after the UI appears.