iOS Recipes - Matt Drance [24]
[infScroller release];
CGRect infFrame = CGRectMake(0, 0, BIG, BIG);
PRPTileView *tiles = [[PRPTileView alloc] initWithFrame:infFrame];
[infScroller addSubview:tiles];
[tiles release];
}
The PRPTiledView class is defined as a subclass of a standard UIView, but to make it a tiling view we need to set its backing layer class to be a CATiledLayer. In this case we actually use a subclass of CATiledLayer, for reasons we’ll look at a bit later.
InfiniteImages/PRPTileView.m
+ (Class)layerClass {
return [PRPTiledLayer class];
}
The initWithFrame: method needs to handle three tasks: setting the tile size, calculating the number of columns, and accessing the iTunes database to create an array of the available albums. We must take into account the possibility of a Retina Display being used on the target device, with its greatly increased resolution. So, we need to use the contentScaleFactor property to adjust the tile size, effectively doubling the size in this example. It is possible that an empty array will be returned from the MPMediaQuery call, but we will check for that later when we create the tile. If necessary, we can draw a placeholder image to fill the gap.
InfiniteImages/PRPTileView.m
- (id)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame])) {
PRPTiledLayer *tiledLayer = (PRPTiledLayer *)[self layer];
CGFloat sf = self.contentScaleFactor;
tiledLayer.tileSize = CGSizeMake(SIZE*sf, SIZE*sf);
MPMediaQuery *everything = [MPMediaQuery albumsQuery];
self.albumCollections = [everything collections];
}
return self;
}
The drawRect: method needs to calculate the exact column and row of the requested tile so that we can pass the position number to the tileAtPosition method. The image we get back from that call is then drawn directly into the specified rect of the tile layer.
InfiniteImages/PRPTileView.m
- (void)drawRect:(CGRect)rect {
int col = rect.origin.x / SIZE;
int row = rect.origin.y / SIZE;
int columns = self.bounds.size.width/SIZE;
UIImage *tile = [self tileAtPosition:row*columns+col];
[tile drawInRect:rect];
}
The tileAtPosition method finds the index of the albumsCollections that we need by calculating the modulus of the position number and the number of albums. Using the representativeItem method, the MPMediaItem class returns a media item whose properties represent others in the collection. This ensures that we get a single image for each album in cases where there are differing images for each track.
The MPMediaItemArtwork class has a convenient method, imageWithSize:, that returns an instance of the album art at exactly the size we need, so we are not required to do any additional scaling of the image to fit the rect. Not all albums have art in the database, and in those cases we load a placeholder image to fill the rect.
InfiniteImages/PRPTileView.m
- (UIImage *)tileAtPosition:(int)position
{
int albums = [self.albumCollections count];
if (albums == 0) {
return [UIImage imageNamed:@"missing.png"];
}
int index = position%albums;
MPMediaItemCollection *mCollection = [self.albumCollections
objectAtIndex:index];
MPMediaItem *mItem = [mCollection representativeItem];
MPMediaItemArtwork *artwork =
[mItem valueForProperty: MPMediaItemPropertyArtwork];
UIImage *image = [artwork imageWithSize: CGSizeMake(SIZE, SIZE)];
if (!image) image = [UIImage imageNamed:@"missing.png"];
return image;
}
We didn’t use the CATiledLayer class earlier to override the layerClass of the view because of a slightly odd feature of the CATiledLayer API. Tiles are normally loaded on a background thread and fade into position over a set duration that defaults to 0.25 seconds. Oddly, fadeDuration is not a property; it is defined as a Class method, so it cannot be modified from the tile layer. To get around this, we need to create a CATiledLayer subclass, PRPTiledLayer, overriding the fadeDuration method, to return the value we want—in this case zero. This makes the new tiles appear immediately but ultimately has little effect on overall scrolling performance.