LiveScore: Content is Everything

February 26th, 2010

I've seen this many times and it's always the same story: content is everything. If you want to secure a future it's not enough to write an excellent app, it's not enough to have a marketing plan; you must get access to attractive content. LiveScore app is an excellent example: at the moment of this writing it is available for several days in App Store and it ranks in Sports category like this:

Country Position
Belgium 1
Croatia 1
Denmark 1
Greece 1
Hong Kong 1
Hungary 1
Indonesia 1
Lebanon 1
Luxembourg 1
Malaysia 1
Portugal 1
Romania 1
Singapore 1
Thailand 1
Turkey 1
Vietnam 1
Austria 2
Czech Republic 2
Ireland 2
Netherlands 2
Poland 2
Slovakia 2
Australia 3
Finland 3
Norway 3
Sweden 3
Switzerland 3
United Arab Emirates 3
Italy 4
Russia 4
UK 4
Argentina 5
Chile 5
Germany 6
France 7
India 10

and so on...

LiveScore

Note that there are no excessive graphic overlays or fancy logos, most of the screen is filled with data. And this data is what people want this application for, that is what they need in the end.

Combined with authentic data sources I believe that this application will be the default source of information about sport scores in App Store. And this should be a model of how developers could organize their business.

UIViews Cache for Reuse

February 19th, 2010

I'm really excited to publish this post because it is about a simple piece of code that noticeably improves the quality of a table view and is quite generic to be used in a broad set of applications. At least it made a difference in the latest app that I'm working on...

We all know that table cells should be reused when number of rows in a table exceeds the number of visible rows. And not only because cells that do not currently fit inside table bounds consume memory but because the time required to create and initialize the cell view is long enough to be noticeable by users. In other words if you do not reuse table cells and flick through the table content then each time before a new row appears you feel a delay.

Now if you agree that cells reuse is good then take a look at UITableView's documentation. Along with cells it is possible to provide section header views. And it may happen that you actually have many sections with handful of cells in them. If you see several such headers on the screen and flick through the table you may feel the delays caused by instantiation of these views. UITableView does not allow to reuse section headers because they are generic UIViews so it seems like you are out of luck with providing a snappy table.

So there was I thinking what to do. How would you approach reuse of UIViews?

  1. Obviously you have to detect when a view could be reused and save it in a queue.
  2. Some form of cache is required to keep reusable views. There could possibly be several view types so you have to have queues for each type limited to a several views in each.
  3. And of course client code should ask for reusable view before creating a new one.

The first one is tricky and actually you can't tell for sure when view can be reused. So I assume that when view is removed from the views hierarchy then I can enqueue it for reuse. And if the view is added later to the views hierarchy I will remove it from the queue. What we need is the method that is invoked on UIView instance when it is added to or removed from the views hierarchy. Effectively this means that view is added to or removed from the window, so I override the following method:

- (void)didMoveToWindow {
  self.window ?
  [[ViewsCache sharedCache] removeReusableView:self] :
  [[ViewsCache sharedCache] enqueueReusableView:self];
}

And this is it. The rest is provided by ViewsCache class. Its API is rather simple and it is basically a wrapper around a dictionary that contains queues of reusable views. I also define a protocol for UIView that adds a reuseIdentifier property that is a type of reusable view. So here is the definition in all its glory:

@protocol ReusableView <NSObject>

@property(copy) NSString *reuseIdentifier;

@end

@interface ViewsCache : NSObject {
  NSMutableDictionary *allViews; // reuseIdentifier -> NSMutableArray:UIView
  NSUInteger capacityPerType;
}

@property(readonly) NSUInteger capacityPerType;

+ (ViewsCache *)sharedCache;

- (UIView<ReusableView> *)dequeueReusableViewWithIdentifier:(NSString *)reuseIdentifier;
- (void)enqueueReusableView:(UIView<ReusableView> *)view;
- (void)removeReusableView:(UIView<ReusableView> *)view;
- (void)clear;

@end

As you see I deliberately chosen the same method signature to dequeue a reusable view from the cache as we use to reuse cells. And here is how the client code looks like:

- (UIView *)tableView:(UITableView *)tableView
  viewForHeaderInSection:(NSInteger)section
{
  NSString *ViewIdentifier = @"HeaderView";
  HeaderView *view = (HeaderView *)[[ViewsCache sharedCache]
    dequeueReusableViewWithIdentifier:ViewIdentifier];
  if (!view) {
    view = [[[HeaderView alloc] initWithFrame:CGRectZero] autorelease];
    view.reuseIdentifier = ViewIdentifier;
  }
  view.textLabel.text = [NSString stringWithFormat:@"Header %d", section + 1];
  return view;
}

Strangely enough I have not noticed any significant increase in FPS when I launched a sample project in Instruments, although I clearly see the effect of this caching technique. Anyway, here is the code: ViewsCache so you can play with it and use as you see fit.

Complementary Headers in UITableView

February 7th, 2010

UITableView is the most used and versatile control in iPhone applications. It is very flexible and sometimes you may not even recognize it within the UI, but it is there and does the job. The customization I describe below is based on an actual customer request. It is more about general views mechanic but I used it in the context of UITableView and so describe it here.

As you may already know the primary content of UITableView is a list of cells. And cells may be grouped in sections, and every section may define a custom header view. If sections contain few cells then several section headers may be visible at the same time. That was the case in the aforementioned request. Additionally, header views were composite and had two labels: the primary title and the secondary informational text. The essence of the request was to show informational text only for the topmost visible section header.

CompHeaders

The key to the solution is the didMoveToWindow method defined in UIView. It is called every time a UIView is added to views hierarchy or removed from it. The rest is simple: implement this method in section header view and when it is called find out a new topmost visible header. To track which view is the one I add complementaryHeader property:

@interface HeaderView : UIView {
  NSString *text;
  NSString *subtext;
  BOOL complementaryHeader;
}

@property(retain) NSString *text;
@property(retain) NSString *subtext;
@property(assign) BOOL complementaryHeader;

@end

Every time section header view is added or removed from window I sort the list of visible views by y coordinate and set this flag for all views except the first one. I also tell views to redraw themselves. In drawRect method the header view checks if this flag is set and does not show informational text if so.

NSComparisonResult CompareHeaderViewsByY(
    HeaderView *view1, HeaderView *view2, void *context) {
  const CGFloat diff = view2.frame.origin.y - view1.frame.origin.y;
  return diff > 0 ? NSOrderedAscending
                  : diff < 0 ? NSOrderedDescending : NSOrderedSame;
}

@implementation HeaderView

@synthesize text, subtext, complementaryHeader;

+ (NSMutableArray *)visibleViews {
  static NSMutableArray *views;
  if (!views) {
    views = [[NSMutableArray alloc] init];
  }
  return views;
}

- (void)didMoveToWindow {
  [super didMoveToWindow];
  NSMutableArray *views = [HeaderView visibleViews];
  if (self.window) {
    if (![views containsObject:self]) {
      [views addObject:self];
    }
  } else {
    [views removeObject:self];
  }
  [views sortUsingFunction:CompareHeaderViewsByY context:NULL];
  HeaderView *upperView = nil;
  for (HeaderView *view in views) {
    if (upperView && EqualStrings(view.text, upperView.text)) {
      view.complementaryHeader = YES;
    } else {
      view.complementaryHeader = NO;
    }
    [view setNeedsDisplay];
    upperView = view;
  }
}

- (void)dealloc {
  [text release];
  [subtext release];
    [super dealloc];
}

- (void)drawRect:(CGRect)rect {
  [[UIColor colorWithWhite:0.7 alpha:1] set];
  UIRectFill(self.bounds);
  if (text) {
    [[UIColor blackColor] set];
    [text drawAtPoint:CGPointMake(10, 5) withFont:[UIFont systemFontOfSize:14]];
  }
  if (subtext && !complementaryHeader) {
    [[UIColor grayColor] set];
    [subtext drawAtPoint:CGPointMake(10, 22) withFont:[UIFont systemFontOfSize:12]];
  }
}

@end

Source code in sample Xcode project is available here.

iPhone App Checklist

February 2nd, 2010

After writing several iPhone applications I've noticed that there are several common things that should be handled within an application. Something is optional but nice to have (like supporting both portrait and landscape orientations), something is mandatory (like responding to connectivity problems) but they are inherent to any application running on the iPhone. Before you start working on a new application or close to finishing one it is a good idea to check the list and think how you handle the particular design aspect or system state. So here goes the checklist:

Interface Orientation

For many applications it is enough to support just one orientation. Application itself is simpler in this case because you don't have to think about stretching table cells and preventing user input during animation. On the other hand you may be creative and use interface orientation as an option for alternative UI like switching between basic and scientific calculator modes or making fonts larger in landscape mode for better readability.

There are several possible values for orientation defined as

typedef enum {
  UIInterfaceOrientationPortrait           = UIDeviceOrientationPortrait,
  UIInterfaceOrientationPortraitUpsideDown = UIDeviceOrientationPortraitUpsideDown,
  UIInterfaceOrientationLandscapeLeft      = UIDeviceOrientationLandscapeRight,
  UIInterfaceOrientationLandscapeRight     = UIDeviceOrientationLandscapeLeft
} UIInterfaceOrientation;

but most applications really do care only about whether it is just portrait or landscape. You can use macros UIInterfaceOrientationIsPortrait(orientation) and UIInterfaceOrientationIsLandscape(orientation) defined in UIApplication.h to check it.

The right place to handle interface orientation is your UIViewController subclass. By default (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation returns YES only for the portrait orientation so if you want to support both then override it to always return YES. There are other goodies like accessors for header and footer views that slide out while interface is rotating and several callback methods that you may override to get notified about changes in interface orientation. Beware though, that if you support both orientations then you should do it consistently, in every view controller. It's not required by OS or any API but will annoy users otherwise.

If you support only landscape mode then you may add UIInterfaceOrientation property to your Info.plist file to tell the OS the initial interface orientation.

Connectivity Problems

That's what will be checked in your application during acceptance in App Store for sure. And yes, I was guilty of not handling this too. It's not required to handle the absence of connectivity in some particular way but the user should be clearly notified that there is no connection to the desired server. If your application depends on data from some particular service, like iTunes, and this service is not reachable then it should be enough to just show a fullscreen warning image that explains that the application can't function without the service. If service is optional or application does not totally depend on it you may just show error dialog each time connection fails. More hints:

  • You can test how your application reacts to connectivity problems by switching on the airplane mode in iPhone preferences.
  • There is an undocumented flag, SBUsesNetwork, that you may set in application's Info.plist file to tell the SpringBoard that your application requires network connectivity. If in airplane mode then OS will show a nice system warning dialog with possibility to go to preferences and switch airplane mode off. Although it's undocumented Apple don't mind us using it.
  • Another useful (and documented) option is UIRequiresPersistentWiFi. Set this flag if you don't want to loose network after 30 minutes.
  • Apple provides sample project named 'Reachability'. There is a useful Reachability class that wraps SCNetworkReachability API.

Saving State On Exit

Normal iPhone applications, like all applications not written by sorcerers working for Apple, can't run in background. Multitasking is simulated by saving the application state on exit and restoring it after the launch. Think of the three steps:

  1. Define what you will remember: current tab, current search text, etc. Store these values in some application-global dictionary; this is your state.
  2. Implement applicationWillTerminate:(UIApplication *)application in application delegate to save the state dictionary in a file.
  3. Implement applicationDidFinishLaunching:(UIApplication *)application in application delegate to read the state dictionary from that file and restore the state.

As far as I know it's not strictly defined by Apple what you should remember. Personally, I think that it is an overkill to save the UI state in most cases, and it may be confusing for a user to relaunch the application after a while and find himself somewhere deep within the navigation hierarchy. What you should save is the user's current data, like search text, because it requires effort to retype.

Low Memory Warning

We all know that we should respond to this hint from the OS by freeing some memory, otherwise it may shutdown our application for good. But it's easy to forget to do so, especially because it is not a visible feature. There are two places where you could react to this situation:

  • didReceiveMemoryWarning in view controller
  • applicationDidReceiveMemoryWarning:(UIApplication *)application in application delegate; good place to flush global caches

Shake Gesture

It can be the essential part of your application but often it is a convenient alternative to some primary control, typically a button. For example I've used it to complement the 'Refresh' button that updates UI with new data from the server.

To support shake gesture you could intercept motion event in view controller. And don't forget to allow this view controller to become the first responder:

- (BOOL)canBecomeFirstResponder {
  return YES;
}

- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event {
  if (motion == UIEventSubtypeMotionShake) {
    [self refreshContent];
  }
}

Preferences

You may declare simple preferences for application by creating a Settings bundle (Xcode even has wizard for this). Options defined there will be available via [NSUserDefaults standardUserDefaults] and you don't have to write any code to show them, just edit properties file as described in Application Preferences.

Device Capabilities

If your application relies on specific device features like GPS or camera you may declare this in UIRequiredDeviceCapabilities property of the application bundle. For more details see Device Support. Many other useful properties are documented in Bundle Programming Guide.

Bundle Version

This one is hard to miss because it is required to publish the application in App Store and they do check that it is specified and increased since the last submission. Typically after 1.0 I fix bugs and add new features and right before publishing the application I review the changes and decide how much to bump the version number. You change it in Info.plist file under CFBundleVersion key.

Submerged Label in UITableViewCell

January 20th, 2010

Apple provides several built-in table cell types in UIKit, but they are not flexible enough to be reused in most applications. On the other hand they give an excellent example about table cells design: AdvancedTableViewCells (you can type "AdvancedTableViewCells" in Xcode help to get it). Here I describe a simple cell with two labels based on a design from this example, but with a twist.

  • The first label is the main text. It could be relatively long and wrap to several lines, but user must see all the text.
  • The second label is a shorter string, something like tag or category that takes a word or two. It should be emphasized by using bold font and dark rounded rectangle as a background.
  • Now is the trickiest part: the second label should be below the first one and aligned to the right. But if the main text does not fill the whole line or wraps to the next line and does not fill it completely so there is enough space for the second label then it should be displayed in this unused space.

Here is a sample application that demonstrates possible cases:

RightLabel

I will not post all the source code here, just highlight the interesting parts. The cell class is ComboCell and here is its declaration:

@interface ComboCell : UITableViewCell {
  UIView *cellContentView;
  NSString *text;
  NSString *subtext;
}

@property(nonatomic, retain) NSString *text;
@property(nonatomic, retain) NSString *subtext;

+ (CGFloat)cellHeightWithText:(NSString *)text
                      subtext:(NSString *)subtext
                        width:(CGFloat)width;

@end

Since height is different for each cell and depends on labels and table width I've added a class method that calculates height given these parameters. In this form it could be called from the table view controller:

- (CGFloat)tableView:(UITableView *)tableView
 heightForRowAtIndexPath:(NSIndexPath *)indexPath {
  return [ComboCell cellHeightWithText:[data objectAtIndex:(indexPath.row * 2)]
                               subtext:[data objectAtIndex:(indexPath.row * 2 + 1)]
                                 width:tableView.bounds.size.width];
}

The advantage of this setup is that code that calculates the cell height is located in the cell class so it could be reused by other table view controllers. The implementation of this function does all the work to calculate the right height and to find place for the second label. You can check the source code to see how it works but the idea behind it is simple:

  1. Calculate the first label size
  2. Calculate the second label size
  3. Create shortest string that has the same or greater width then the second label
  4. Calculate size of the first label combined with this string
  5. If height of the combined string is the same as the height of the first label then there is a space for the second label at the end of the first label

It may sound like a lot of work but I cache those shortest strings so performance is good.

There is no function in CoreGraphics to add a path for rounded rectangle, so I'm using the following function:

static __inline__ void CGContextAddRoundedRect(
    CGContextRef c, CGRect rect, int corner_radius) {
  CGFloat x_left = rect.origin.x;
  CGFloat x_left_center = rect.origin.x + corner_radius;
  CGFloat x_right_center = rect.origin.x + rect.size.width - corner_radius;
  CGFloat x_right = rect.origin.x + rect.size.width;
  CGFloat y_top = rect.origin.y;
  CGFloat y_top_center = rect.origin.y + corner_radius;
  CGFloat y_bottom_center = rect.origin.y + rect.size.height - corner_radius;
  CGFloat y_bottom = rect.origin.y + rect.size.height;
  CGContextBeginPath(c);
  CGContextMoveToPoint(c, x_left, y_top_center);
  CGContextAddArcToPoint(c, x_left, y_top, x_left_center, y_top, corner_radius);
  CGContextAddLineToPoint(c, x_right_center, y_top);
  CGContextAddArcToPoint(c, x_right, y_top, x_right, y_top_center, corner_radius);
  CGContextAddLineToPoint(c, x_right, y_bottom_center);
  CGContextAddArcToPoint(c, x_right, y_bottom, x_right_center, y_bottom, corner_radius);
  CGContextAddLineToPoint(c, x_left_center, y_bottom);
  CGContextAddArcToPoint(c, x_left, y_bottom, x_left, y_bottom_center, corner_radius);
  CGContextAddLineToPoint(c, x_left, y_top_center);
  CGContextClosePath(c);
}

Note that I declare it as static __inline__ in the header file. Alternatively you may use CG_INLINE macro.

Xcode project is stored in my samples repository, enjoy!

Java Templates In Xcode Organizer

January 14th, 2010

Apple is famous for its attention to details, but not everyone is perfect...

Java Templates In Xcode Organizer

Alternating Rows for UITableView

December 1st, 2009

Tables support in iPhone is thought out well but it seems that due to the complexity of the issue they have dropped out support for alternating rows. I suppose that this is a popular feature so I've written a subclass of UITableViewController that could be used as a drop-in replacement that changes background color of the odd rows.

AltTableViewController.h

#import <UIKit/UIKit.h>

@interface AltTableViewController : UITableViewController {
  UIColor *primRowColor, *altRowColor;
}

@property(retain) UIColor *primRowColor, *altRowColor;

- (BOOL)isAltRow:(NSIndexPath *)indexPath;
- (void)updateRowsLook;

@end

AltTableViewController.m

#import "AltTableViewController.h"

@implementation AltTableViewController

@synthesize primRowColor, altRowColor;

+ (UIColor *)defaultAltRowColor {
  static UIColor *color;

  if (!color) {
    color = [UIColor colorWithRed:0 green:0 blue:1 alpha:0.05];
  }
  return color;
}

- (UIColor *)altRowColor {
  return altRowColor ? altRowColor :
      [AltTableViewController defaultAltRowColor];
}

- (UIColor *)primRowColor {
  return primRowColor ? primRowColor : [UIColor whiteColor];
}

- (void)dealloc {
  [altRowColor release];
  [primRowColor release];
  [super dealloc];
}

- (BOOL)isAltRow:(NSIndexPath *)indexPath {
  return indexPath.row == 0 || indexPath.row % 2 == 0;
}

- (void)updateRowsLookLater {
  NSArray *indexPaths = [self.tableView indexPathsForVisibleRows];
  if (indexPaths) {
    for (NSIndexPath *indexPath in indexPaths) {
      if ([self isAltRow:indexPath]) {
        [self.tableView cellForRowAtIndexPath:indexPath].backgroundColor =
            self.altRowColor;
      } else {
        [self.tableView cellForRowAtIndexPath:indexPath].backgroundColor =
            self.primRowColor;
      }
    }
  }
}

- (void)updateRowsLook {
  [self performSelector:@selector(updateRowsLookLater)
      withObject:nil afterDelay:0.01];
}

- (void)tableView:(UITableView *)tableView
        willDisplayCell:(UITableViewCell *)cell
        forRowAtIndexPath:(NSIndexPath *)indexPath {
    if ([self isAltRow:indexPath]) {
        cell.backgroundColor = self.altRowColor;
    } else {
        cell.backgroundColor = self.primRowColor;
  }
}

- (void)tableView:(UITableView *)tableView
    commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
    forRowAtIndexPath:(NSIndexPath *)indexPath {
  [self updateRowsLook];
}

- (void)tableView:(UITableView *)tableView
    moveRowAtIndexPath:(NSIndexPath *)fromIndexPath
    toIndexPath:(NSIndexPath *)toIndexPath {
  [self updateRowsLook];
}

@end

Note that your cells should not contain labels with opaque backgrounds. Background color of the cell should be visible through labels.

I have also implemented methods that are called in response to editing actions since now deletion, insertion and rearrangement of a row impacts other rows. If your table will be editable you should call these methods to update backgrounds of all cells.

Daily Deviations: Know Your Users

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

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

November 4th, 2009

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.