Archive for November, 2009

Daily Deviations: Know Your Users

Friday, November 20th, 2009

I'm thinking about adding more features to the Daily Deviations app because many users expect more from it. Initially I planned it to be a simple browser for the daily deviations channel (hence the name) but many users request search, access to profiles and other parts of the site. So before adding new features I decided to be scientific and gather some data with the help of Pinch Analytics on how current features are used. My two primary questions are:

  • Which parts of the application do users actually use? If there are some buttons that users just do not click on then I could remove them and make the UI better.
  • Which hardware and software do they have? I'm especially interested in OS versions because it allows me to use new features of the OS without loosing users.

Integration was very easy: you initialize analytics library on startup and shutdown it on exit. To record user actions you notify 'beacon' and this is one line of the code. I suppose you are not interested in my beacons so I will show just hardware and software statistics:

Devices and OS versions

The charts represent 647 unique users. I require at least 3.0 version of the OS so obviously there are no users with lower OS versions. The general picture was as I had expected it to be though not without surprises.

  • The number of iPhone 3GS devices is really big; I thought it would be around 20% with iPhone 3G leading the chart.
  • iPods are ⅓ of the whole swarm and it makes sense to think of them when you design your application. I have not expected them to be that active in using App Store.
  • I suppose 'Unknown' device is me using simulator.
  • Most of the users do upgrade to the latest version of the OS and this is a pleasant surprise. I think it is pretty safe to use the latest OS features.

Update after a week or so

I'm gathering data for a while now and it seems that distribution is roughly the same. One more thought about iPod users: they have to pay for upgrade to OS 3.0 and I think there should be users that just don't do this. These users are not shown here since I require at least OS 3.0 and loose them.

Devices and OS versions

Writing Gadget for Google Wave

Sunday, November 15th, 2009

Recently I got a Google Wave account and could not help myself to play with it. The most interesting feature for me is the shared state associated with every wave. This allows to embed multi-user widgets into the wave (in Google terminology gadgets inside wavelet). All other widgets I know are designed for a single user; for example Mac OS X dashboard widgets are written using the same web technologies and even have access to some system functionality but essentially they are just the tiny web pages. Google wave gadgets are designed with many concurrent users in mind and this makes them really the tiny web applications.

So I decided to write a gadget that is good for several people, and the best candidate seems to be a simple game. Five in a row was a perfect candidate since it requires only and images. It also has a fixed 15 by 15 board and simple win condition.

Google provides a brief but comprehensive guide on the topic of writing gadgets and this is the first thing that you should read. From the developer perspective gadget is composed like this:

  • XML wrapper that describes the gadget and lists its dependencies, name, height and other properties
  • HTML elements that comprise the gadget UI; you will need at least one div that you will use as an anchor to build dynamic UI upon
  • JavaScript code to handle user input and state updates

Physically gadget is an XML file with a Content element that wraps all HTML and JavaScript in CDATA section. This is not convenient because syntax highlighter and other features of dedicated HTML and JavaScript editors do not work for embedded content. I would prefer to have several files with html and js extensions that could be referenced from the XML file. This way it would be easier to create widgets that target several environments. Now if I would decide to make this game available as a Mac OS X dashboard widget I have to create a separate JavaScript file and duplicate the game logic.

There is also one difficult place in the API: concurrent modification of the shared state. The documentation is very succinct on the point:

Not only is a Wave gadget's state shared among all Wave participants, but in the typical case, any participant can change the gadget's state at any time. If two users change values for different keys at the same time, the wave resolves it. However, if the value for the same key is changed, only one change goes through.

What if two users start the game at the same time? After user clicks on the New Game button I send delta with several properties. All property names are the same in two deltas and I think one of the deltas will be just discarded, but if there would be unique property names the state would be corrupted. What is still not clear for me is how they determine when to discard the second delta. I assume they discard it if it comes before all participants are notified about the first delta but it would be helpful to know for sure.

The game logic is simple and I will not put it here except one caveat: full state restore. When game state changes wave sends you an update by invoking a callback function that you register during initialization. During normal gameplay I process only the last move for efficiency but when user switches from another wave the field is clear and I have to perform the full state restore.

  function stateUpdated() {
    var state = wave.getState();
    var move = state.get('move');
    if (!window.g_xoStateFullyRestored) {
      fullStateRestore();
      window.g_xoStateFullyRestored = 1;
    } else {
      if (move) {
        var col = state.get('col' + move);
        var row = state.get('row' + move);
        var cell = cellAt(col, row);
        if (move % 2) {
          makeXCell(cell);
        } else {
          makeOCell(cell);
        }
      } else {
        var board = document.getElementById('board');
        var cells = board.getElementsByTagName('div');
        for (var i = 0; i < cells.length; i++) {
          clearCell(cells.item(i));
        }
      }
    }
  }

I have also designed the game logic in a way that allows users to play both sides at the same time. The same user can start a game and join it but if it is played by two distinct users then of course each user can do moves only of its side.

xo5.png

You can also check this game in the Google wave samples gallery.

Overall I like the platform and would be interested in its success. I have not tried all the features yet but hope to find some time for it. I think the next thing I would try to do is to write a robot that can play this game.

Oh, and here is link to the gadget if you would like to give it a try: http://dizbits.googlecode.com/svn/trunk/xo5/xo5.xml

Remote Image for iPhone

Wednesday, November 4th, 2009

This code is now a part of the BaseAppKit. See BARemoteImageView class.

In one of my earlier posts I've written about HTML parser for Daily Deviations, an iPhone application that shows feed with daily images from http://www.deviantart.com/. I use it to extract links to the images, their descriptions and other pages. It's easy to show textual descriptions but images require more elaborate handling. Naive approach is to take a list of urls and load them in a loop, but it leads to the following problems:

  • UI gets blocked until all images are loaded. For slow connections application may become unresponsive for a long period of time and users may decide to just shut it down.
  • There may be not enough memory to keep all the images. In the middle of the loop application may be shut down by the OS because it allocated all the memory.
  • Even if the application did manage to load all the images OS may ask it to free some memory later. With plain UIImages we don't have an appropriate facility to release the images and reload them later when necessary. In case of Daily Deviations I just release all the images and when user scrolls the grid missing images got loaded on demand.

Standard solution in this case is to create a proxy object that will load and release the image representation when necessary. For this purpose I've created the RemoteImage class. When the image is not available the proxy may return some default image to indicate that the remote image is not loaded yet or is missing.

Helper Classes

RemoteImage class is pretty generic but does require a couple of helper classes. The first one is NetworkActivityController: its purpose is to wrap access to the network activity indicator. System network activity indicator is a tiny animated icon that is displayed by OS in the system bar. Applications should set it visible when they transmit data over the network. The purpose of the NetworkActivityController is to count a number of network operations so after the first request indicator is made visible and on completion of the last request it is hidden.

//
//  NetworkActivityController.h
//

#import <Foundation/Foundation.h>

@interface NetworkActivityController : NSObject {
  NSUInteger level;
}

+ (NetworkActivityController *)sharedController;
- (void)startActivity;
- (void)stopActivity;
- (void)stopAllActivity;

@end

//
//  NetworkActivityController.m
//

#import "NetworkActivityController.h"

@implementation NetworkActivityController

+ (NetworkActivityController *)sharedController {

  static NetworkActivityController *instance;

  if (!instance) {
    instance = [[NetworkActivityController alloc] init];
  }
  return instance;
}

- (void)startActivity {
  if (++level == 1) {
    [[UIApplication sharedApplication]
      setNetworkActivityIndicatorVisible:YES];
  }
}

- (void)stopActivity {
  if (level == 0) {
    return;
  }
  if (--level == 0) {
    [[UIApplication sharedApplication]
      setNetworkActivityIndicatorVisible:NO];
  }
}

- (void)stopAllActivity {
  level = 0;
  [[UIApplication sharedApplication]
    setNetworkActivityIndicatorVisible:NO];
}

@end

The second class is LoadImageOperation. This class extends NSOperation and represents a task to load remote image that could be performed asynchronously. What happens in Daily Deviations when user opens a feed? HTML page is scanned for images' urls and for each image a new LoadImageOperation is created and placed in the queue. OS picks these operations and performs them asynchronously notifying UI when they complete. User sees a grid of images' placeholders that are filled with images as they are loaded. At the same time user is free to scroll the grid and navigate within the application.

//
//  LoadImageOperation.h
//

#import <UIKit/UIKit.h>

#define kImageKey @"image"

@interface LoadImageOperation : NSOperation {
  NSURL *url;
  SEL selector;
  id target;
  NSMutableDictionary *context;
}

- (id)initWithURL:(NSURL *)anUrl
    selector:(SEL)aSelector
    target:(id)aTarget
    context:(NSMutableDictionary *)aContext;

@end

//
//  LoadImageOperation.m
//

#import "LoadImageOperation.h"

@implementation LoadImageOperation

- (id)initWithURL:(NSURL *)anUrl
    selector:(SEL)aSelector
    target:(id)aTarget
    context:(NSMutableDictionary *)aContext {
  if (self = [super init]) {
    url = [anUrl retain];
    selector = aSelector;
    target = [aTarget retain];
    context = [aContext retain];
  }
  return self;
}

- (void)dealloc {
  [url release];
  [target release];
  [context release];
  [super dealloc];
}

- (void)main {
  NSURLResponse *response;
  NSData *data = [NSURLConnection
    sendSynchronousRequest:[NSURLRequest requestWithURL:url]
    returningResponse:&response
    error:NULL];
  if ([self isCancelled]) {
    return;
  }
  UIImage *image = nil;
  if (data) {
    image = [UIImage imageWithData:data];
  }
  if (image) {
    [context setObject:image forKey:kImageKey];
  }
  [target performSelectorOnMainThread:selector
    withObject:context waitUntilDone:NO];
}

@end

Remote Image

RemoteImage class is the implementation of the image proxy that wraps UIImage and manages its lifecycle. It provides the following features:

  • Dedicated NSOperationQueue shared by all remote images. Each LoadImageOperation is performed on this queue. Why this is an advantage is not obvious but if you consider how Daily Deviations operates it becomes clear. Each day site publishes a list of selected images, also known as daily deviations. User can change the date and see the page for that day. If not all images are loaded for the current day yet and user changes the date Daily Deviations cancels all operations in the queue thus freeing the bandwidth for the next batch of images. With a single queue it is one method call: [[RemoteImage queue] cancelAllOperations].
  • Ability to quickly unload all images. Application should use it when memory is low to drop all remote images' data. It's also possible to lock particular images so they are not unloaded. Daily Deviations for example locks the remote image that is viewed in the details screen so thumbnails could be unloaded if the detailed image is too big.
  • Notifications on status change. When image starts loading, is loaded, is unloaded or is failed to load RemoteImage posts a notification using default NSNotificationCenter. All UI components that are interested in the state of the remote image could listen to these events and update themselves accordingly.

Two main properties of the RemoteImage are link and image. Link is the URL of the remote image and can be changed anytime, typically during creation of the RemoteImage instance. Image property is read-only and points to the UIImage instance when loaded. There is no property for the placeholder image since it really depends on UI component but you may add one if necessary.

//
//  RemoteImage.h
//

#import <Foundation/Foundation.h>
#import "LoadImageOperation.h"

typedef unsigned int RemoteImageStatus;
enum {
    kRemoteImageUnloaded,
    kRemoteImageLoading,
    kRemoteImageFailedToLoad,
    kRemoteImageLoaded
};

// Sent only when image is loaded, failed to load or was unloaded.
extern NSString * const RemoteImageStatusChangedNotification;

@interface RemoteImage : NSObject {
  NSString *link;
  NSInteger width;
  NSInteger height;
  UIImage *image;
  LoadImageOperation *operation;
  RemoteImageStatus status;
  BOOL locked;
}

 // url of the image
@property(copy) NSString *link;

// assumed size; typically from img element
@property(assign) NSInteger width, height;

// actual image
@property(readonly) UIImage *image;

// image state
@property(readonly) RemoteImageStatus status;

// if true then image is used and should not be unloaded
@property(assign) BOOL locked;

+ (void)unloadAllImages;
+ (void)cancelLoadingAllImages;

- (void)load;
- (void)loadWithPriority:(NSOperationQueuePriority)priority;
- (void)unload;

@end

//
//  RemoteImage.m
//

#import "RemoteImage.h"
#import "NetworkActivityController.h"

NSString * const RemoteImageStatusChangedNotification =
  @"RemoteImageStatusChangedNotification";

@implementation RemoteImage

@synthesize link, width, height, image, status, locked;

+ (NSMutableArray *)allRemoteImages {
  static NSMutableArray *images;

  if (!images) {
    images = [[NSMutableArray alloc] init];
  }
  return images;
}

+ (void)unloadAllImages {
  for (RemoteImage *image in [RemoteImage allRemoteImages]) {
    [image unload];
  }
}

+ (NSOperationQueue *)queue {
  static NSOperationQueue *queue;

  if (!queue) {
    queue = [[NSOperationQueue alloc] init];
  }
  return queue;
}

+ (void)cancelLoadingAllImages {
  [[RemoteImage queue] cancelAllOperations];
  [[NetworkActivityController sharedController] stopAllActivity];
}

- (id)init {
  if (self = [super init]) {
    status = kRemoteImageUnloaded;
    [[RemoteImage allRemoteImages] addObject:self];
  }
  return self;
}

- (void)imageLoaded:(NSMutableDictionary *)context {
  [[NetworkActivityController sharedController] stopActivity];
  [image release];
  image = [[context objectForKey:kImageKey] retain];
  [operation release];
  operation = nil;
  status = image? kRemoteImageLoaded : kRemoteImageFailedToLoad;
  [[NSNotificationCenter defaultCenter]
    postNotificationName:RemoteImageStatusChangedNotification object:self];
}

- (void)load {
  [self loadWithPriority:NSOperationQueuePriorityNormal];
}

- (void)loadWithPriority:(NSOperationQueuePriority)priority {
  if (operation && [operation isCancelled]) {
    [operation release];
    operation = nil;
    status = kRemoteImageUnloaded;
  }
  if (status != kRemoteImageUnloaded) {
    return; // already loaded
  }
  status = kRemoteImageLoading;
  NSURL *url = (link) ? [NSURL URLWithString:link] : nil;
  if (!url) {
    status = kRemoteImageFailedToLoad;
    [[NSNotificationCenter defaultCenter]
      postNotificationName:RemoteImageStatusChangedNotification object:self];
    return;
  }
  NSMutableDictionary *context = [NSMutableDictionary dictionary];
  operation = [[LoadImageOperation alloc] initWithURL:url
                         selector:@selector(imageLoaded:)
                         target:self
                        context:context];
  [operation setQueuePriority:priority];
  [[RemoteImage queue] addOperation:operation];
  [[NetworkActivityController sharedController] startActivity];
}

- (void)unload {
  if (self.locked) {
    return;
  }
  if (operation) {
    [operation cancel];
    [operation release];
    operation = nil;
  }
  if (image) {
    [image release];
    image = nil;
  }
  status = kRemoteImageUnloaded;
  [[NSNotificationCenter defaultCenter]
    postNotificationName:RemoteImageStatusChangedNotification object:self];
}

- (void)dealloc {
  [[RemoteImage allRemoteImages] removeObject:self];
  self.locked = NO;
  [self unload];
  [super dealloc];
}

@end

Conclusion

RemoteImage is pretty generic and useful class. You may use it freely in your applications and if you will find any errors or make any significant improvements please consider writing me email.