links for 2010-06-28

  • Have you ever thought “I wish there was a way to click a link in an email and launch my app…” Well there is, through the magic of custom app URLs. The basic idea is this: register a custom URL protocol with the iPhone, and if someone clicks a link with that protocol on that device, it will launch your app. But wait, there’s more: because it’s a URL, you can pass parameters into the app on launch.

Rocketfish fitted case for the iPhone 4

Like many iPhone 4 owners, I find that the signal drops when I hold it a certain way, so I went shopping for a case today. Apple’s bumper case seems to be sold out in many places, so after looking at several other options, I decided on Rocketfish’s soft gel fitted case, available for $20 at Best Buy (with my reward card, it actually cost me $16). Unlike Apple’s bumper case, which only protects the sides, this fitted case protects the sides and back of the iPhone 4 and allows access to all ports & controls. It’s available in several colors, but the black one looks especially nice with a black iPhone. It fits nicely and the iPhone snaps in & out easily. This is a really nice looking case that offers good protection without adding too much bulk to the iPhone.

DSC_0618

Rocketfish Soft Gel Fitted Case for iPhone 4

Rocketfish Soft Gel Fitted Case for iPhone 4

Members of Tinariwen detained in Denver

Members of the group Tinariwen have posted a message on their Facebook page saying that they were stopped at Denver airport on their way to DC and arrested by TSA. Unfortunately this is an all-too-common situation for artists visiting the US.

Screen shot 2010-06-25 at 1.15.33 PM.png

If you haven’t heard of Tinariwen, they’re a west African group of Toureg nomads who play some of the most hypnotic, intoxicating music.

Memory Upgrade

I upgraded my new MacBook Pro to 8GB of RAM today ($295 from OWC, plus a rebate when I return my old RAM). It makes a huge difference when running many applications or large applications such as Aperture.

The upgrade was very easy and the Unibody MacBook Pro is very easy to open, despite having 3 screws to remove. Once the bottom is off, the RAM is exposed and can be easily replaced.

Inside my MBP

The entire process took about 15 minutes, including opening & closing the case.

I often run Xcode, Interface Builder & Photoshop CS5 at the same time, which caused the disk to thrash constantly in 4GB. Now there’s hardly any swapping and I haven’t seen a beachball. Aperture 3 was so slow it was nearly unusable in 4GB, but with 8GB it feels pretty snappy. I can move between photos seeing a beachball.

Memory stats

iPhone 4

My iPhone 4 arrived today, ahead of schedule. Syncing it took several hours, since I had over 3000 tracks to copy,

The screen really lives up to the hype, although I find that it has a yellowish cast. When viewed next to my 3GS, the difference is quite dramatic.

iPhone 3GS Screen iPhone 4 Screen

A close up of the screens shows a huge difference.
iPhone 3GS Detail

iPhone 4 Detail

It’s a bit thinner than the 3GS and completely flat.

iPhone 3GS & iPhone 4

The photo quality is good enough that I probably won’t carry a point & shoot camera any more, although it’s no match for a DSLR.

First photo from iPhone 4

A full gallery is available here.

Offer accepted & contract signed

My offer on the condo in San Leandro was accepted and I have a signed contract with the seller. Since this is a short sale, it will take at least 6-8 weeks for approval and it still might be rejected, depending on the appraisal. The place won’t be ready for move-in right away, since the current owner is taking the appliances.

IMG_0304.jpg

IMG_0305.jpg

IMG_0310.jpg

 

Why I chose SQLite instead of Core Data

A lot has been written about the relative benefits of Core Data or SQLite in an iPhone app. In most cases, Core Data is easier and has some performance benefits, but the data store is difficult to create & modify from outside of your application. SQLite, on the other hand, has some excellent tools like SQLiteManager for creating & editing data. Therefore, if you have a large amount of pre-loaded data that you want to be able to edit easily, SQLite makes more sense.

This is exactly the case with Removr. Game levels are defined by an object that specifies the background image and a bitmap of the layout of pieces on the screen. Although Core Data could easily fetch the object as needed in Removr, it would be very difficult for an external level editor to create those objects and update the persistent store.

My level object happens to be very simple and can easily be mapped to a database structure.

// Level.h
@interface Level : NSObject {
    NSData * _map;
    NSString * _background;
    NSNumber * _index;
}

@property (nonatomic, retain) NSData * map;
@property (nonatomic, retain) NSString * background;
@property (nonatomic, retain) NSNumber * index;

@end

// Level.m
#import "Level.h"

@implementation Level
@synthesize map = _map, background = _background, index = _index;

- (void)dealloc
{
    self.map = nil;
    self.background = nil;
    self.index = nil;
    [super dealloc];
}
@end

The corresponding database structure looks like this:

CREATE TABLE levels (
	ix integer NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
	background text,
	map blob NOT NULL
);

Fetching the objects from sqlite takes more code than using Core Data but isn’t too difficult. I need to deal with two different SQLite3 objects for the database connection and the prepared query. Rather than interpreting the SQL query every time you execute it, you will prepare the query once and execute it as many times as you want.

A good introduction to the SQLite3 C interface is available here.

Here is my level manager. My init method opens the database connection and prepares the query that will be used later. Note that the query includes a ‘?’ for variable substitution.

The GetLevel method is where the interesting stuff happens. The function sqlite3_bind_int() tells it which level number we’re looking for and sqlite3_step() actually executes the query. In this case, I’m only interested in a single row, but in many cases it will be called repeatedly as long as it returns SQLITE_ROW. I then use the sqlite3_column functions to extract data from the result row and populate my Level object. Finally, calling sqlite3_reset() will allow the prepared statement to be reused.

// LevelManager.h
@interface LevelManager : NSObject {

    NSString *_dbpath;

    sqlite3 * db;
    sqlite3_stmt * query;
}

@property (retain,nonatomic) NSString *dbpath;

@end

// LevelManager.m
#import "LevelManager.h"
#import "Level.m"

@implementation LevelManager

@synthesize dbpath = _dbpath;

- (id) init
{
    if ((self = [super init])) {
        self.dbpath = [[NSBundle mainBundle] pathForResource:@"levels" ofType:@"sqlite3"];
        sqlite3_open([self.dbpath UTF8String] , &db);

		// this query will be used to obtain a level from the database.
		// The '?' will be replaced with the level number when we perform the query
        sqlite3_prepare_v2(db, "SELECT * FROM levels WHERE ix=?", -1, &query, NULL);

    }
    return self;
}

- (void)dealloc
{
    sqlite3_finalize(query);
    sqlite3_close(db);

    self.dbpath = nil;

    [super dealloc];
}

- (Level*)GetLevel: (int)number
{
    Level *lvl = nil;

	// specify the level number we want for the query
    sqlite3_bind_int(query, 1, number);

	// request a row from the query result
    if (sqlite3_step(query) == SQLITE_ROW) {
        void *blob;
        int nbytes;

		// first, create a new level object
        lvl = [[Level alloc] init];

		// integer columns are easy
        lvl.index = [NSNumber numberWithInt: sqlite3_column_int(query, 0)];

		// string columns are a bit more complex since we need to convert a C string to a NSString
        lvl.background = [NSString stringWithCString:(char*)sqlite3_column_text(query, 1)
                                                                 encoding:NSUTF8StringEncoding];

		// BLOB columns require two calls to obtain the actual data and the length
        blob = (void*)sqlite3_column_blob(query,2);
        nbytes = sqlite3_column_bytes(query,2);

		// we use the bytes & length to create a NSData
        if (blob && (nbytes > 0)) {
            lvl.map = [NSData dataWithBytes:blob length:nbytes];
        }
    }

	// get ready to reuse the query
    sqlite3_reset(query);

	// we should return an autoreleased object
    return [lvl autorelease];
}

@end