Archive for 2008

Custom UIWebView Navigation Controller

Download Drill Down Example
+ Controller Source
(1.6MB .zip)

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 the community part of 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 I didn’t want to write a communication layer to download and parse images and custom XML from the server.
Read more

Stopping a Runaway WebView

It turns out, all of the WebViews you create on the iPhone are managed by a central object called WebCore. It seems to be some sort of singleton that takes care of loading content and managing resource loading threads – but it can lead to some interesting problems if you try to rapidly create, display, and release a series of WebViews.

The upcoming version of NetSketch uses WebViews to display community content, but it wraps the web content in an iPhone native navigation bar. That way, there’s a feeling of “drilling down” into pages in the community. (Think: facebook app). It makes it hard to tell the entire thing is  done with WebViews and you get some more user-friendly behavior (like using the top right button in the navigation bar to login/logout). After several iterations, I chose to implement this “drilldown” approach using a custom navigation bar and separate WebViews for each page. A “navigation stack” holds the WebViews of previous pages, and if you drill down too far, they are converted to UIImages and the actual WebViews are released. This behavior helps limit memory consumption, while allowing you to perform common actions quickly (going back and clicking another thumbnail, for example). Keeping images of very old views allows you to rapidly exit the interface without seeing blank pages or waiting for old pages to reload.

Unfortunately, my implementation seems to have run afoul of WebCore. It turns out that if you request a page in a WebView and attempt to release the WebView before it’s done loading, WebCore will attempt to send it delegate messages once it’s been destroyed – and throw exceptions all over the place. My first instinct was to call [webView stopLoading], but that is (apparently) asynchronous and doesn’t actually stop the WebCore from preparing some of the content. If you register an object as a WebView delegate, you’ll notice that you can still recieve webViewDidFinishLoad:(UIWebView *)webView after a stopLoading call.

I messed around with this for a while, and was almost ready to create a “ready-to-release” stack for webViews that were still loading. As a last resort, I tried calling [webView loadRequest: nil], and got some promising results. When you call loadRequest, the WebView  makes an asynchronous call to

[sourcecode language='c']

- (BOOL)webView:(UIWebView *)w shouldStartLoadWithRequest:(NSURLRequest *)r navigationType:(UIWebViewNavigationType)navigationType

[/sourcecode]

When this call returns true, the WebView attempts to load the URL and realizes it is nil. It will almost immediately call – (void)webView:(UIWebView *)w didFailLoadWithError:(NSError *)error, and then it is officially DEAD. You can safetly release the WebView and its delegate without running the risk of further callbacks hitting deallocated objects.

This isn’t a perfect solution (read: it’s a ridiculous hack). It makes stopping and releasing a WebView a multi-step asynchronous process. However, unlike the [webView stopLoading] approach, it results in a consistent series of delegate callbacks that you can observe and also appears to work 100% of the time.

In my implementation, the navigation controller tells a WebView wrapper object to “unlink” the WebView. It detaches itself from everything and calls loadRequest:nil on the WebView. When the didFailLoadWithError: delegate call is received, the WebView is released, the delegate connection is broken and the wrapper is automatically released as well.

So far, so good.

But I still wonder – what exactly does stopLoading do?

The NDA is gone!

Apple has finally removed the NDA from non-beta iPhone software, so the community can finally come out of hiding. I can’t wait to actually – you know – talk to people! I’ve got a few things in the pipes for NetSketch, and hopefully I can post some tutorials and info here. Then this blog might actually be useful…

- Ben

NetSketch drawing on a shirt!

l0k1 posted a picture of this t-shirt in the NetSketch forums yesterday – made from a NetSketch drawing by another artist. Pretty cool! It’s nice to see stuff like this get put to use – I wasn’t sure anybody was actually using the “Download to Illustrator” option on the site. Seeing stuff like this really makes it worth going the extra mile for features like that. The shirt looks great!

iArtMobile went live earlier this week and it looks pretty cool. There’s a gallery where you can view artwork created on the iPhone. I’m a bit biased, but I think the NetSketch stuff looks the best :-)

I discovered another drawing app, Brushes, while I was looking at iArtMobile. I downloaded it and played around – and I think it’s the strongest pixel-based drawing app I’ve seen. It’s ridiculously fast and does textured brushes – something that a lot of people have been asking for in NetSketch. Of course, the tradeoff is that the drawing is only 320×480 and, although you can zoom, you can’t draw in any more detail once you zoom in. Oh well… It looks like they built a custom UIScrollView similar to the one in NetSketch, so the pan/zoom gestures are familiar! It’d be nice if we could standardize on that. Some of the other apps – like No.2. use some truly bizarre controls.

Finally…

I broke a lot of stuff this weekend working on NetSketch. I’m not all that familiar with threading, and I wanted to add some progress indicators for long redraws. I ended up doing it a couple times, but it finally works. (You can cancel it half way through the redraw process and everything). For the longest time it was leaking memory, but it turns out I just forgot to call [pool release] when the thread received a cancel message. Oops :-)

Opening a 6MB drawing and immediately interrupting the drawing – no leaks! Finally…

Another great review!

I did a Google search for NetSketch a couple minutes ago (c’mon – I have to do it every day, right?) and I found this great review: http://www.appleiphoneapps.com/2008/07/review-netsketch/. 4.75 stars out of 5! I’m psyched :-) . It’s a bummer that the App Store pushes traffic off the web into iTunes, because I think people don’t read a whole lot online about the apps they buy. Could be wrong though – and good reviews certainally can’t hurt!

I’ve made some pretty huge changes to the app this week, including a few fixes that will give it a nice big performance boost :-) I was using an NSDictionary to store the strokes in memory, and then sorting them by key (a timestamp) when I needed to do a full redraw. The sort was necessary, because strokes coming across the network might be placed out of timestamp order, causing incorrect layering. I scanned through the entire drawing architecture, and it turns out I wasn’t performing key->value lookups in the dictionary very often. When they were being performed, the stroke being requested was usually one of the last few added to the drawing. I converted the NSDictionary to two NSArrays – one of keys and one of values. I decided to keep the entire structure sorted all the time, and wrote a modified binary search function so the most popular key-value lookups were really fast.

Should be a good update :-)

Maximum brush size in Flash

I’ve been having a lot of trouble with Flash recently. This weekend, I discovered a line size limitation that doesn’t seem to be documented anywhere. Unfortunately, my.netsketchapp.com was already live and people’s drawings were being rendered incorrectly. The goal was to create an online Flash viewer that replicated the behavior of NetSketch on the iPod. The iPod app supports infinite pan and zoom, and often it’s not possible to render things to a jpg. It’s just not the same. I wrote a small flash “viewer” that loads an XML version of the drawing and allows you to pan and zoom through the drawing the same way you do on the iPod.

Here’s a sample of the Flash viewer:

http://my.netsketchapp.com/drawing.php?id=205

The problem is, lineStyle() in ActionScript 2.0 will only let you make lines with a stroke width less than 235. Yes… 235. I have no idea why, but I tested at increments of 5 and after 235 the line just stops getting bigger. In other words, these two statements produce the same result:

1
2
line.lineStyle(235, color, 100, false, "normal", "round","round", 2);
line.lineStyle(300, color, 100, false, "normal", "round","round", 2);

The fix turned out to be pretty easy. There’s a backend application that converts NetSketch’s binary vector format into XML, and I added another pass to the conversion process that scaled the drawing so all the strokes were thinner than 235pt… I was already doing some processing to fit the drawings within an Illustrator compatible EPS document (maximum size of 16383 x 16383). Still, it’s frustrating that Flash has such odd limitations. I haven’t experimented much with ActionScript 3.0, and I’m hoping that may resolve some of these things…

Feedback

So it’s week 3 for NetSketch! I got a few awesome reviews on iTunes yesterday that basically made my day:

“Quite a good app. Has not crashed yet, which is better than every single other app I have. Well worth it, especially considering how reachable the developer is and how frequently it is getting updated with new features.”- Crabpot8

“Their website is a fantastic addition to an already great app. I’m on it almost every day to check out other people’s art.” – Obscurum

Somehow, that makes not sleeping for a few weeks totally worth it.  Apple released some sales figures for apps on the store yesterday, and it looks like NetSketch has been running about 60 sales a day for the last week or so. Older data isn’t available (yet?) so I’m not sure how the Facebook ads and initial release went. Guess we’ll see!

Scaling a CGImage

I was working on NetSketch last night and ran into an interesting problem. To optimize the display of thumbnail images, I decided to cache them at their display size and not at full size (320×380). It worked out really well and led to a huge performance boost at launch. The allocation of memory is so slow on the iPhone that leaving around a bunch of large images just isn’t an option – especially when they all need to be loaded at the same time.

I wrote a convenience function to do the heavy lifting, and I’ve posted the source below. Given a CGImageRef and a desired scale (1.0 being the same), it returns another CGImage of a different scale. Of course, for many scenarios you can just shove the full size image into a UIImage and let UIKit resize the image for you. That approach doesn’t perform anti-aliasing, though – and will give you a slightly more grainy result.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
 
CGImageRef CreateScaledCGImageFromCGImage(CGImageRef image, float scale)
{
// Create the bitmap context
CGContextRef    context = NULL;
void *          bitmapData;
int             bitmapByteCount;
int             bitmapBytesPerRow;
 
// Get image width, height. We'll use the entire image.
int width = CGImageGetWidth(image) * scale;
int height = CGImageGetHeight(image) * scale;
 
// Declare the number of bytes per row. Each pixel in the bitmap in this
// example is represented by 4 bytes; 8 bits each of red, green, blue, and
// alpha.
bitmapBytesPerRow   = (width * 4);
bitmapByteCount     = (bitmapBytesPerRow * height);
 
// Allocate memory for image data. This is the destination in memory
// where any drawing to the bitmap context will be rendered.
bitmapData = malloc( bitmapByteCount );
if (bitmapData == NULL)
{
return nil;
}
 
// Create the bitmap context. We want pre-multiplied ARGB, 8-bits
// per component. Regardless of what the source image format is
// (CMYK, Grayscale, and so on) it will be converted over to the format
// specified here by CGBitmapContextCreate.
CGColorSpaceRef colorspace = CGImageGetColorSpace(image);
context = CGBitmapContextCreate (bitmapData,width,height,8,bitmapBytesPerRow,
colorspace,kCGImageAlphaNoneSkipFirst);
CGColorSpaceRelease(colorspace);
 
if (context == NULL)
// error creating context
return nil;
 
// Draw the image to the bitmap context. Once we draw, the memory
// allocated for the context for rendering will then contain the
// raw image data in the specified color space.
CGContextDrawImage(context, CGRectMake(0,0,width, height), image);
 
CGImageRef imgRef = CGBitmapContextCreateImage(context);
CGContextRelease(context);
free(bitmapData);
 
return imgRef;
}