Archive for February, 2010

LiveScore: Content is Everything

Friday, 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

Friday, 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

Sunday, 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.