Posts from the “Blog” Category

Drill Down WebView Navigation

The next version of NetSketch will include a community browser, allowing you to view uploaded drawings, watch replays, and leave comments without leaving the app. When I started working on the community interface, I looked to other apps for inspiration. Almost every app I’ve used on the iPhone use a sliding navigation scheme, giving you the feeling that you’re drilling down into content as you use the application. This interface is intuitive in a lot of contexts, and dates back to the original iPod. The Facebook app allows you to browse other people’s facebook pages and uses a drill down navigation bar. This works well for the social-network space because you can drill down to look at information and then return to the first page quickly.

I decided to use a UINavigationBar and implement a similar drill-down interface for NetSketch. However, I didn’t want to create custom controllers for each page in the community. I wanted to be able to improve the community without updating the app, and didn’t want to write a communication layer to download and parse images and custom XML from the server.

Using a UIWebView seemed like the obvious choice. It could make retrieving content more efficient, and pages could be changed on the fly. With WebKit’s support for custom CSS, I could make the interface look realistic and comprable to a pile of custom-written views.

I quickly realized that it wasn’t all that easy to implement “drill down” behavior with a UIWebView. Early on, I ruled out the possibility of creating a mock navigation bar in HTML. Since Safari on the iPhone doesn’t support “position:static” or “position:fixed” CSS tags, there was no good way to make the bar sit at the top of the screen. I decided that a native UINavigationBar would be more practical and provide a better user experience. However, UINavigationController was built to use separate controllers for each layer, and doesn’t worry about freeing up memory when the stack of controllers gets big. I thought it was important that a maximum of eight UIWebViews were in memory at once, since Mobile Safari obeys that limitation and because pages could potentially be very large.

I tried several solutions, and finally created a custom DrillDownWebController class with a manually managed UINavigationBar to handle the interface. The class maintains a “stack” of DrillDownPages, with each page representing a single layer in the drill-down hierarchy. It can be a root level controller, or it can be loaded into an existing UINavigationController. When it appears, it silently swaps its parent’s navigation bar with it’s own.

The DrillDownPage is a wrapper for a UIWebView that acts as its delegate and provides higher-level access to important properties of the page, such as it’s title. When the user clicks a link in a web view, a new DrillDownPage object is created and it begins loading the requested page in an invisible UIWebView. The controller displays an activity indicator in the top right corner of the navigation bar, and slides in the new page when it finishes loading. All the other pages in the page “stack” are notified that their position in the stack has changed.

The notification step is important, because it allows the Page objects to

The Best WordPress Site Ever?

So I accidentally clicked an ad this afternoon and stumbled across Ecoki.com, an online community for eco-friendly folks. I hadn’t even scrolled half way down their home page when I found myself thinking: “What was this built in?” Ecoki is quite possibly the best designed wordpress site I’ve ever seen. I had to look at the page source to figure it out.

http://ecoki.com/

It looks like it’s a completely custom template. Must have cost a fortune… Great look though!

PNG compression in Android… (You have got to be kidding)

Over the last few weeks, I’ve been learning the Android SDK in an effort to bring Layers to Android devices. It’s going pretty well, but every once and a while I run into a truly WTF moment.

Tonight I was importing some images from the iPhone version of Layers when I noticed that Android seems to visibly reduce the quality of PNG files at compile time. In images with fine gradients, smooth color transitions, or very light shadows you tend to get banding. It almost looks like the image were being converted to a GIF file.

I figured it’d be easy to fix. Go into Eclipse, right click on everything, look in menus… repeat… profit! Unfortunately, it seems there’s no way to turn off compression for specific file or choose a non-lossy compression method. However, I found this gem of a solution in the Android Widget Design Guidelines:

“To reduce banding when exporting a widget, apply the following Photoshop Add Noise setting to your graphic.”

Um… what now?

It turns out, you can get around the compression algorithm by adding a small amount of pixel noise to your images. It’s mostly invisible, and it prevents the compression from producing obvious bands.

It’s an instant fix, but I almost laughed out loud. Seriously? This is the documented solution? *sigh*.

iPhone Development Tutorial

I gave a presentation within the Engineering school on Friday that gave a brief look at the iPhone platform and Objective-C. The end of the presentation was a quick tutorial in Interface Builder and XCode. You can download the presentation and the tutorial project here:

iPhone Development Tutorial

iPhoneDevelopmentTutorial.zip

If you have any questions, feel free to email me at [email protected] or post a comment. Also, be sure to check out layersforiphone.com.

App Store Piracy: Worse than you think.

Two weeks ago, a minor update to Layers hit the App Store. The update included several important bug fixes and a few features, but one of the most major changes was the addition of a piracy tracking system. Each time the app is used on a jailbroken device, it phones home with a few (anonymized) metrics so that I can track the spread of pirated copies. Software on the Layers server gathers the data and prints out some cool statistics.

Technically cool, that is. Not really “cool” at all. I’ve sold around 1,500 copies of Layers this week and in the same 7 day period, more than 1,780 copies have been pirated. It’s flattering, to some extent; people obviously enjoy the app. However, it’s also evidence to a much larger problem that I feel Apple continues to overlook. The DRM used in iPhone apps hasn’t been changed in ages, and an app on a jailbroken device can be automatically cracked using another iPhone app in a matter of seconds. No command line tools. No hand-editing files. You double click the app’s icon and it cracks it. Done.

For “expensive” apps like Layers, piracy is an especially significant problem. The latest version of Layers runs about 22,000 lines of code, and my community and target market are small. Everyone needs to chip in so I can recoup the cost of development and rationalize extra time spent improving the app. The App Store’s layout and “Top 100″ formatting encourage 99¢ apps with limited utility, so it’s difficult to market a $4.99 drawing app to begin with. (I’ve been lucky enough to be a Staff Favorite on the App Store) Piracy rates above 100% really don’t help.

So what do you do? I feel it’s absolutely necessary to weed out the pirates. I wouldn’t mind providing illegitimate users with a time-limited or feature-limited version of the app. The problem is, current methods of screening for pirated copies are binary-dependent and patchable. In about a week, Layers will start displaying notices to pirates asking them to upgrade their “demo” copies of the app to a full version or “buy me a beer.” I hope that a few people will appreciate the app enough after using the pirated copy to consider paying. Even a 5% pirated-to-paid conversion rate would be an extra 15 sales a day.

In the future, I’d like to see Apple implement a secure model for confirming that users are licensed to use an app. A secure receipts model is built in to the In-App Purchase system, and I don’t see why it wouldn’t work for the app as a whole. The app would establish a secure connection to an iTunes server, exchange product identifiers and account details, and verify that a product had in fact been purchased. I don’t think Apple will implement anything in the near future, because it would require admitting that piracy was, in fact, an issue. We can dream, though.

A few other developers I’ve talked to have suggested creating a repository of device identifiers that have been nabbed during phone-home routines. It seems like a good idea, but I understand there’s some hesitation to start calling people pirates left and right. Apps would need to pre-emptively contact the repository, and a simple change to the UNIX hosts file could break the system.

So for now, It looks like I’ll be dreaming of an extra 1,700 sales a week.

4 1/2 stars! But you can’t compete with sex…

Layers finally dropped off the list of top paid entertainment apps on the App Store this week. It’s not all that surprising – for such an expensive app, I was happy to see it on there at all! I think this screenshot pretty much sums up my feelings, though. Of the top 100 entertainment apps, 13 of them are some variant of “hot babe” apps. Looks like you just can’t compete with sex.

EXIF Orientation Flags and the iPhone Camera

Layers just came out this past Monday, and it has this great feature that allows you to add a layer to a drawing from your iPhone’s photo library. Simple enough – right? Apple provides the UIImagePicker API, we call a couple functions and get an image back. For most purposes, that would work great! Write some code, test, commit, done. The problem is, the picker interface allows the user to adjust the scale/positioning of the image, and the cropped image is always returned at 320x320px (or less)…

Mac OS X Stack Overflow Status Item!

I’ve become a huge fan of Stack Overflow over the last few weeks. The community there is helpful and fast and there are quite a few questions about Cocoa and Objective-C! It’s gotten to the point where I visit SO whenever my code is compiling – so I thought it was time to take matters into my own hands and make things easier to follow.

I’ve made a Stack Overflow status item for Mac OS X (also a “menu bar item” or a “system icon”) that shows your reputation and lists questions on the front page containing your interesting tags:

SO Status Item in Action

It’s pretty primitive at this point – you can click a question to view it, or click the tiny arrow to go to your user page. It updates every 90 seconds using the RSS and ATOM feeds from the site, so the most active questions are always available at a glance. It’s compatible with Mac OS X 10.5 and 10.6 – so download it and give it a shot! I know there are a lot of things that could be added – so leave a comment and let me know what you think.

Download the Stack Overflow Status Item for Mac OS X (0.5 MB)

PackBits algorithm in Objective-C

The PackBits algorithm is one of the TIFF data compression methods, and it’s also used for pixel data in Photoshop PSD and TGA files. It was originally developed for MacPaint, and although it’s still widely used, there isn’t a whole lot of information online about it. I spent some time this weekend writing a category to extend the NSData class and support the packBits algorithm, and I think I’ve finally got it.

PackBits is really dirt simple. If you have a string of hex values like 00 00 00 FF FF FF FF, it replaces series of  3 or more identical bytes with a count, and then the repeated byte. The input above would be 03 00 04 FF. A prefix is also attached to series of different bytes so the decoder knows how many bytes to pass through before looking for another header byte.

The full documentation of PackBits can be found on Wikipedia. My implementation is a modified version of the one available here: http://michael.dipperstein.com/rle/index.html (source link at very bottom of page). That implementation is a modification of the official PackBits algorithm that allows slightly larger runs of data to be encoded in a single header + byte pair. I was interested in the original implementation only (since data written from any customized encoder could not be opened by a standard decoder) so I changed it back to the standard algorithm.

The primary function provided in the PackBitsAdditions category below is:

[objc] – (NSData*)packedBitsForRange:(NSRange)range skip:(int)skip[/objc]

This function returns the packBits representation of the data in “range”, advancing “skip” bytes with each read. For instance, to read and encode every byte in “range”, you would provide skip = 1. To read and encode only every 4th byte, you would pass skip = 4. This may seem like an odd implementation, but it’s very handy when you need to encode the channels of an RGBA image separately (as in PSD format).

The other function in the category is packedBitsDescription. It describes the packed bits and the process that would be followed to decode them. This function could be easily extended to actually decode the data.

If you find this code useful, please leave a comment! I debugged this for quite a while, and I’d be happy to help if you run into any issues with my implementation!

NSDataPackBitsAdditions.h

[objc]

@interface NSData (PackBitsAdditions)

- (NSString*)packedBitsDescription;
- (NSData*)packedBitsForRange:(NSRange)range skip:(int)skip;

@end
[/objc]

NSDataPackBitsAdditions.m

[objc]
@implementation NSData (PackBitsAdditions)

- (NSString*)packedBitsDescription
{
NSMutableString * description = [NSMutableString string];
char * row = (char*)[self bytes];
int pbOffset = 0;
int pbResultBytes = 0;

while (pbOffset < [self length]){
int headerByte = (int)row[pbOffset];
if (headerByte < 0){
int repeatTimes = 1-headerByte;
UInt8 repeatByte = (UInt8)row[pbOffset+1];
[description appendFormat: @"Printing %u %d times. ", repeatByte, repeatTimes];

pbResultBytes += repeatTimes;
pbOffset += 2;
} else if (headerByte >= 0){
[description appendFormat: @"Printing %d literal bytes. ", headerByte + 1];
pbResultBytes += headerByte + 1;
pbOffset += 2 + headerByte;
}
}

[description appendFormat: @"Total: %d bytes decoded.", pbResultBytes];
return description;
}

- (NSData*)packedBitsForRange:(NSRange)range skip:(int)skip
{
char * bytesIn = [self bytes];
int bytesLength = range.location + range.length;
int bytesOffset = range.location;
NSMutableData * dataOut = [NSMutableData data];

BOOL currIsEOF = NO;
unsigned char currChar; /* current character */
unsigned char charBuf[MAX_READ]; /* buffer of already read characters */
int count; /* number of characters in a run */

/* prime the read loop */
currChar = bytesIn[bytesOffset];
bytesOffset = bytesOffset + skip;
count = 0;

/* read input until there’s nothing left */
while (!currIsEOF)
{
charBuf[count] = (unsigned char)currChar;
count++;

if (count >= MIN_RUN){
int i;

/* check for run charBuf[count - 1] .. charBuf[count - MIN_RUN]*/
for (i = 2; i <= MIN_RUN; i++){
if (currChar != charBuf[count - i]){
/* no run */
i = 0;
break;
}
}

if (i != 0){
/* we have a run write out buffer before run*/
int nextChar;

if (count > MIN_RUN){
/* block size – 1 followed by contents */
UInt8 a = count – MIN_RUN – 1;
[dataOut appendBytes:&a length:sizeof(UInt8)];
[dataOut appendBytes:&charBuf length:sizeof(unsigned char) * (count - MIN_RUN)];
}

/* determine run length (MIN_RUN so far) */
count = MIN_RUN;
while (true){
if (bytesOffset < bytesLength){
nextChar = bytesIn[bytesOffset];
bytesOffset += skip;
} else {
currIsEOF = YES;
nextChar = EOF;
}
if (nextChar != currChar) break;

count++;
if (count == MAX_RUN){
/* run is at max length */
break;
}
}

/* write out encoded run length and run symbol */
UInt8 a = ((int)(1 – (int)(count)));
[dataOut appendBytes:&a length:sizeof(UInt8)];
[dataOut appendBytes:&currChar length:sizeof(UInt8)];

if ((!currIsEOF) && (count != MAX_RUN)){
/* make run breaker start of next buffer */
charBuf[0] = nextChar;
count = 1;
} else {
/* file or max run ends in a run */
count = 0;
}
}
}

if (count == MAX_READ){
int i;

/* write out buffer */
UInt8 a = MAX_COPY – 1;
[dataOut appendBytes:&a length:sizeof(UInt8)];
[dataOut appendBytes:&charBuf[0] length:sizeof(unsigned char) * MAX_COPY];

/* start a new buffer */
count = MAX_READ – MAX_COPY;

/* copy excess to front of buffer */
for (i = 0; i < count; i++)
charBuf[i] = charBuf[MAX_COPY + i];
}

if (bytesOffset < bytesLength)
currChar = bytesIn[bytesOffset];
else
currIsEOF = YES;
bytesOffset += skip;
}

/* write out last buffer */
if (0 != count){
if (count <= MAX_COPY){
/* write out entire copy buffer */
UInt8 a = count – 1;
[dataOut appendBytes:&a length:sizeof(UInt8)];
[dataOut appendBytes:&charBuf length:sizeof(unsigned char) * count];
}
else
{
/* we read more than the maximum for a single copy buffer */
UInt8 a = MAX_COPY – 1;
[dataOut appendBytes:&a length:sizeof(UInt8)];
[dataOut appendBytes:&charBuf length:sizeof(unsigned char) * MAX_COPY];

/* write out remainder */
count -= MAX_COPY;
a = count – 1;
[dataOut appendBytes:&a length:sizeof(UInt8)];
[dataOut appendBytes:&charBuf[MAX_COPY] length:sizeof(unsigned char) * count];
}
}

return dataOut;
}

@end
[/objc]