Sometimes It’s the Obvious

Between working on a couple updates for our existing apps, I’ve been working on learning some new stuff with the iPhone SDK.  Sometimes it’s great to just wipe the canvas and start fresh!  So, I’m playing around with which UI* framework elements support custom artwork and creating some custom controls.  It’s fun, especially now that I’ve got some solid footing on the default appearance and behaviors of the common controls.

I tossed together a UITabBar app template and started filling out the custom UIViewControllers.  The code to push the view controller is pretty standard:

   if (myView == nil)
   {
      myView = [[UIViewController alloc] initWithNibName:@"testView" bundle:nil];
   }

   [self.navigationController pushViewController:myView animated:YES];

 

I created a corresponding Objective-C UIViewController class, testView, and associated the class in Interface Builder, including the “view” connection. 

testView 

The test view controller had a couple IBOutlets which I also created in IB and associated too. 

Save all… recompile in Xcode, debug – and Boom!  “NSUnknownException”, reason: “[<UIView 0x47a3e1>  setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key testControl.”

Nothing like a helpful error message, eh?

So – a quick return to Interface Builder where I double check the connections, Class Identity, and assorted properties that might trigger the exception.  Save all… recompile in Xcode, debug, and Boom!  Crashed again. 

Okay… time to get more serious.  Since these were empty classes anyway, I blew them away and started fresh.  Recreated the classes, the controls, the connections, and tried again.  No difference.  Ugh.  I swear I’ve done this before – a lot!

Well, I decided to pull out all of the controls except a UITableView and toss breakpoints throughout the UIViewController class to try to trap and step through exactly where things might be going amiss.  Recompile, debug, and things worked great!  The view appeared, including the table, but none of my breakpoints caught.

This was very odd, as I had breaks in initWithNibName:bundle:, loadView, viewDidLoad, and – just to be thorough – viewWillAppear:.  A little head scratching and I came to the quick conclusion that my custom class, despite being set in Interface Builder, isn’t actually being used.  And, after a few more minutes of triple-checking my settings, I discovered the source of the problem – I’d created a UIViewController instead of a “testView” (see above).  Changing the code fixed the problem – big surprise.

While I overlooked my declaration initially (as perhaps you did too!) – it just goes to show that sometimes it’s the obvious and before nuking work that probably is okay – give a quick inspection all around.

Security Updates, Xcode & Subversion

In addition to my full time job as a developer, I get occasional opportunities to moonlight as a system administrator.  Recently, we updated our Macs to the latest security patch Apple pushed out – and lo, our subversion integration with Xcode broke.  Sigh.

Back when we were initially setting up boxes for development, I very dimly recall there was some “shell magic” via the Terminal that needed to happen to get all the parts working together.  I think this was largely because Xcode preferred to work with an earlier version of subversion, perhaps 1.4.x – and it was no longer readily available at the time.

None the less, it’s easy to forget all the configuration steps in setting up a developer box – especially stuff that takes only a couple hours to figure out a nearly a year ago.  We’re not yet to Snow Leopard, so our tools may be a little dated: Xcode is 3.1.4 and Subversion is 1.5.5.

Symptoms

After the security upgrade, we fired up Xcode and noticed that there were:

  • No modified files in the project.
  • Right clicking a source file showed only “Get Annotations” in the Groups & Files pane.
  • Viewing the SCM | Repositories dialog (from the main menu) gave an error: 180001 unable to connect to the repository.
  • Even more scary was the Terminal message: svn: Expected FS format ‘2’; found format ‘3’

Solutions

Unfortunately, determining the solution wasn’t particularly clear.  Doing some Google searches on the error messages produced limited results.  The initial reaction was that something went wrong with the repository, but it was just working!  Running, “svnadmin verify” against the repository produced the File System format error message which was ominous.

The first thought was that perhaps the security patch affected the PATH variable.  Mac OS X ships with an older version of the subversion binaries and maybe we were picking up the wrong one.  On the Mac, various shell variables including your path are set in the .profile file found in your home directory.  A quick in the Terminal showed that /usr/bin was first in the path and /usr/local/bin (home to our newer svn) was last.  No problem – a quick edit and all was better.  Well… not quite.

Adjusting the path restored subversion from the Terminal and even the svnX app.  But, Xcode still stoutly refused to acknowledge the existence of the repository.  Grrr!  But, a step in the right direction and at least our repository was okay.

The breakthrough was trying to “Get Annotations” on a file.  There was a much more descriptive error – “155021 (Unsupported working copy format) please get a newer Subversion client.”  This jogged something loose in my mind and I dimly recall having to change some of the system libraries to get Xcode working with the newer Subversion.

After another quick search I found two other folks reported the same thing (Paul Solt and Blind Genius).  I’ll restate the solution here, but effectively Xcode is dynamically linking with the older Subversion libraries found in /usr/lib.  To work with newer builds of Subversion, you’ll need to create symbolic links to the newer dynamic libraries as follows:

cd /usr/lib

sudo mkdir svn.orig

sudo mv libap*-1.dylib svn.orig

sudo mv libsvn*-1.dylib svn.orig

sudo mv libap*-1.0.dylib svn.orig

sudo mv libsvn*-1.0.dylib svn.orig

sudo ln –s /opt/subversion/lib/*-1.0.dylib .

sudo ln –s /opt/subversion/lib*-1.dylib .

After adding the symbolic links, restart Xcode and things should be back to normal.

Cautions

One thing worth reverting is the PATH change.  Modifying the search order for executables might be considered a security risk as often 3rd party binaries (particularly shell ones) traditionally reside in /usr/local/bin and you might end up running something unexpected.  Perhaps safer is to create symbolic links to the binaries in /usr/bin, or alternatively command aliases in your .profile to run the correct version.

Common SQLite Operations

In nearly any program with persistent data that can be manipulated, I find myself writing three types of routines: insert, update, and delete. In the example below, I focus on using SQLite to perform these operations on some generic data.

To create a simple table for demonstration purposes, we can use the command-line ‘sqlite3’ utility.  In one of my previous posts, I discuss how to automate the (re)generation of your database, but this example is contrived enough that it doesn’t warrant such attention.  In the shell, use the following commands:

   $> sqlite3 db.sqlite
   sqlite> CREATE TABLE myObject (primaryKey INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, amount NUMERIC);
   sqlite> .quit
   $>

Getting this new database into your XCode project, and adding the SQLite3 framework would be next, but I’m going to skip over it, as it’s covered elsewhere and not difficult to put together.

Here’s a simple Objective-C object as the data model for our newly created table.

MyObject.h

@interface MyObject : NSObject
{
    NSInteger primaryKey;
    NSString *name;
    float amount;
}

@property (nonatomic, assign, readonly) NSInteger primaryKey;
@property (nonatomic, retain) NSString *name;
@property (nonatomic, assign) float amount;

// Returns: new MyObject with primary key
- (id)initWithPrimaryKey:(NSInteger)pk database:(sqlite3*)db;

// Updates the database with the object values
- (void)updateDatabase:(sqlite3*)db;

// Delete an object with primary key 'pk'
+ (void)removeWithPrimaryKey:(NSInteger)pk database:(sqlite3*)db;

// Returns: primary key of newly inserted object
+ (NSInteger)addMyObjectIntoDatabase:(sqlite3*)db;

// Clean up, called on app termination
+ (void)finalizeStatements;

@end

The class defines “insert” using addMyObjectIntoDatabase:, “update” using updateDatabase:, and “delete” with removeWithPrimaryKey:database:

The class also has a initialization member presumably called by the controller that has better visibility to all of the table elements.  I’ll fill in the method bodies, but leave the actual hook-up to higher level classes as an “exercise for the reader”.

In the pattern above, generally the controller class will issue a “SELECT primaryKey FROM myObject” statement, then iterate through the returned result.  With each row, the controller factory method creates a new MyObject using the row’s primary key.  But, there are a number of valid approaches depending on your implementation requirements.

Here’s the implementation body of the MyObject class.  I took the design choice of preparing the various queries and storing the result as a class static pointer.  The advantage is performance since the queries are parameterized and “pre-compiled” for SQLite.  The disadvantage is that they consume resources that must be released at some point – in our case using the finalizeStatements method.

MyObject.m

static sqlite3_stmt *init_MyObj_statement = nil;
static sqlite3_stmt *update_MyObj_statement = nil;
static sqlite3_stmt *delete_MyObj_statement = nil;
static sqlite3_stmt *insert_MyObj_statement = nil;

@implementation MyObject

@synthesize primaryKey;
@synthesize name;
@synthesize amount;

- (id)initWithPrimaryKey:(NSInteger)pk database:(sqlite3*)db
{
   if (self = [super init])
   {
      primaryKey = pk;
      if (init_MyObj_statement == nil)
      {
         const char *sql = "SELECT * FROM myObject WHERE primaryKey=?";
         int result = sqlite3_prepare_v2(db, sql, -1, &init_MyObj_statement, NULL);
         NSAssert1(result == SQLITE_OK, @"initWithPrimaryKey: failed to prepare statement with err '%s'", sqlite3_errmsg(db));
      }

      sqlite3_bind_int(init_MyObj_statement, 1, primaryKey);

      if (sqlite3_step(init_MyObj_statement) == SQLITE_ROW)
      {
         int columnID = 1; // 0 is primary key
         char *nameStr = (char*)sqlite3_column_text(init_MyObj_statement, columnID++);
         name = (nameStr) ? [[NSString stringWithUTF8String:nameStr] retain] : @"";

         amount = sqlite3_column_double(init_MyObj_statement, columnID);
      }

      sqlite3_reset(init_MyObj_statement);
   }

   return self;
}

- (void)updateDatabase:(sqlite3*)db
{
   if (update_MyObj_statement == nil)
   {
      const char *sql = "UPDATE myObject SET name=?, amount=? WHERE primaryKey=?";
      int success = sqlite3_prepare_v2(db, sql, -1, &update_MyObj_statement, NULL);
      NSAssert1(success == SQLITE_OK, @"updateDatabase: failed to prepare with message '%s'", sqlite3_errmsg(db));
   }

   int columnID = 1;

   sqlite3_bind_text(update_MyObj_statement, columnID++, [name UTF8String], -1, SQLITE_TRANSIENT);
   sqlite3_bind_double(update_MyObj_statement, columnID++, amount);
   sqlite3_bind_int(update_MyObj_statement, columnID++, primaryKey);

   int success = sqlite3_step(update_MyObj_statement);
   NSAssert1(success == SQLITE_DONE, @"updateDatabase: update failed with message '%s'", sqlite3_errmsg(db));

   sqlite3_reset(update_MyObj_statement);
}

+ (void)removeWithPrimaryKey:(NSInteger)pk database:(sqlite3*)db
{
   if (delete_MyObj_statement == nil)
   {
      static char *sql = "DELETE FROM myObject WHERE primaryKey=?";
      int result = sqlite3_prepare_v2(db, sql, -1, &delete_MyObj_statement, NULL);
      NSAssert1(result == SQLITE_OK, @"removeWithPrimaryKey: failed with message '%s'", sqlite3_errmsg(db));
   }

   sqlite3_bind_int(delete_MyObj_statement, 1, pk);
   int success = sqlite3_step(delete_MyObj_statement);
   NSAssert1(success == SQLITE_DONE, @"removeWithPrimaryKey: failed with message '%s'", sqlite3_errmsg(db));
   sqlite3_reset(delete_MyObj_statement);
}

+ (NSInteger)addMyObjectIntoDatabase:(sqlite3*)db
{
   if (insert_MyObj_statement == nil)
   {
      static char *sql = "INSERT INTO myObject (name) VALUES ('')";
      int result = sqlite3_prepare_v2(db, sql, -1, &insert_MyObj_statement, NULL);
      NSAssert1(result == SQLITE_OK, @"addMyObjectIntoDatabase: failed to prepare statement with err '%s'", sqlite3_errmsg(db));
   }

   int success = sqlite3_step(insert_MyObj_statement);
   if (success != SQLITE_ERROR)
   {
      return sqlite3_last_insert_rowid(db);
   }

   NSAssert1(0, @"addMyObjectIntoDatabase: failed with message '%s'", sqlite3_errmsg(db));
   return -1;
}

+ (void)finalizeStatements
{
   if (init_MyObj_statement)
      sqlite3_finalize(init_MyObj_statement);
   if (update_MyObj_statement)
      sqlite3_finalize(update_MyObj_statement);
   if (delete_MyObj_statement)
      sqlite3_finalize(delete_MyObj_statement);
   if (insert_MyObj_statement)
      sqlite3_finalize(insert_MyObj_statement);
}

@end