Removr is now feature complete. I’m only waiting for the level maps & I’m still working on the sound effects with xfxr. In the next week or so I hope to send out the first beta.
iOS and Mac developer
Removr is now feature complete. I’m only waiting for the level maps & I’m still working on the sound effects with xfxr. In the next week or so I hope to send out the first beta.
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
PicSlide uses a table that lets you choose a picture to play with. For each picture it shows a thumbnail. When the thumbnails are loaded from the application’s resources, you don’t have to be too concerned with image loading time.However, when I load those thumbnails from the web, as I do now with the Magic Panda, scrolling the table can become extremely slow.
Previously, I was loading the thumbnail directly from my table data source’s cellForRowAtIndexPath: method.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"tvCell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { [[NSBundle mainBundle] loadNibNamed:@"tvCell" owner:self options:nil]; cell = tvCell; self.tvCell = nil; } NSString *url = [picUrls objectAtIndex: indexPath.row]; UIImageView *img = (UIImageView*)[cell viewWithTag: 2]; img.image = [UIImage imageWithData: [NSData dataWithContentsOfURL:[NSURL URLWithString: url]]]; }
If you don’t have too many rows, a NSMutableDictionary makes a quick & easy image cache. I simply added one method to implement image caching:
NSMutableDictonary *imageCache; - (UIImage *)getCachedImage: (NSString*)url { UIImage* theImage = [imageCache objectForKey:url]; if ((nil != theImage) && [theImage isKindOfClass:[UIImage class]]) { return theImage; } else { theImage = [UIImage imageWithData: [NSData dataWithContentsOfURL:[NSURL URLWithString: url]]]; [imageCache setObject:theImage forKey:url]; return theImage; } }
When I load my table, I now call getCachedImage rather than fetching the image directly.
img.image = [self getCachedImage: url];
The result can be seen below:
I just submitted the update to PicSlide 1.6. This version adds one new feature: Magic Pandas.
Flickr has Magic Pandas that vomit rainbows and photos. If you ask them nicely, they’ll give you some of the most interesting pictures they can find, which PicSlide will let you play with and even save to your photo album.
Cheezburger Networks finally has an App Store account, and we’re in the process of setting up provisioning so I can resubmit the app. When it returns, it will be listed under Cheezburger Networks and will no longer be sold by me.
UPDATE: It will be listed under Pet Holdings. It’s built with the new app ID & provisioning, and will hopefully be submitted this week (I don’t have the ability to submit apps with their account).
UPDATE #2: It’s still “in progress” with no ETA. They also said:
plz start sending comments to our customer service page if you would
I just submitted iDjembe 2.1, the first universal version. Even though the old version didn’t look too bad on an iPad, the iPad optimized graphics look nicer. It also sounds a lot better on an iPad.
I have two app updates coming up in the next few days:
The artwork for Removr is complete, but we haven’t finished designing all of the levels.
I no longer own I Can Has Cheezburger. When it returns, it will be sold by Cheezburger Networks instead of by me. They’re still in the process of getting their company developer account approved.
You might have seen Patrick Mandia’s post about upcoming projects, which included this screenshot:
Removr is one of the two cocos2d-based games I’m working on. Patrick is working with Toffeenut Design on the artwork, while I’m doing the coding.
As you may have noticed, I Can Has Cheezburger app is no longer available. I have removed it from sale and it will be re-submitted by Cheezburger network when their app store account is set up. Unfortunately this means updates will no longer be available for anyone who bought the app from me. I’d rather not have people continue to buy it and then feel ripped off when they can’t update.
I will continue to develop the app as a contractor for Cheezburger networks, although I will no longer be selling it myself.
It’s official – I’m going to WWDC. I have my ticket and I booked my flights & hotel.
I’ll be staying a few extra days to look at some homes in the bay area and hopefully buy one. I’ve decided on San Leandro for its convenient location and reasonable home prices. Most places in San Leandro have a pretty high walk score, and it’s 30 minutes from San Francisco.