UIScrollView (Tiled)

To follow up on last week’s post about UIScrollViews, I want to explore this somewhat mysterious passage from the documentation:

The object that manages the drawing of content displayed in a scroll view should tile the content’s subviews so that no view exceeds the size of the screen. As users scroll in the scroll view, this object should add and remove subviews as necessary.

I hope that the following demo will clearly demonstrate the desired behavior. (You might also check out this very helpful post from Matt Gallagher.)

The Goal

I want to create a UIScrollView containing a very large (let’s say 16000 x 11500 point) virtual view, which can be panned about quickly.

The Code

You can download an Xcode project that builds the demo here: it’s a moderately-modified version of the View-Based Application template.

Tiles

The key part of this demo is the management of 4 TileView objects by the main view controller. The TileView class is a subclass of UIView, and its only interesting method is drawRect:

- (void)drawRect:(CGRect)rect
{
	CGContextRef context = UIGraphicsGetCurrentContext();
    	[[UIColor blackColor] set];
	
	// Labeling the view
	NSInteger r = (NSInteger) (self.frame.origin.y/self.frame.size.height);
	NSInteger c = (NSInteger) (self.frame.origin.x/self.frame.size.width);
	NSString* s = [NSString stringWithFormat:@"(%d, %d)",c,r];
	
	UIFont* f = [UIFont systemFontOfSize:18];
	CGContextSelectFont(context, [[f fontName] cStringUsingEncoding:NSASCIIStringEncoding], [f pointSize], kCGEncodingMacRoman);
	CGContextSetTextDrawingMode(context, kCGTextFillStroke);
	CGContextSetTextMatrix(context, CGAffineTransformMakeScale(1, -1));
	
	CGContextShowTextAtPoint(context,
				 0,
				 [f pointSize],
				 [s cStringUsingEncoding:NSMacOSRomanStringEncoding],
				 [s lengthOfBytesUsingEncoding:NSMacOSRomanStringEncoding]);
}

The main view controller creates an NSMutableArray containing 4 of these tiles, and tracks this array in its tiles member. The controller creates these tiles in its createTiles method, invoked from viewDidLoad:

- (void)createTiles
{
	if (tiles) [self removeTiles];
	
	TileView* a[4];
	for (NSInteger i = 0; i <= 3; i++)
	{
		a[i] = [[TileView new] autorelease];
		
		a[i].backgroundColor = [UIColor grayColor];
		
		[self.scrollView addSubview:a[i]];
	}
	tiles = [[NSMutableArray alloc] initWithObjects:a count:4];
}

Scrolling

As you might suspect, the heart of the code is the main view controller’s implementation of the scrollViewDidScroll: UIScrollViewDelegate method:

- (void)scrollViewDidScroll:(UIScrollView*)scrollView
{
	// Tile width and height (derived from scrollView)
	CGFloat w = self.scrollView.bounds.size.width;
	CGFloat h = self.scrollView.bounds.size.height;
	
	// Row and column of the UL tile
	NSInteger ul_tile_x = (NSInteger) self.scrollView.contentOffset.x/w;
	NSInteger ul_tile_y = (NSInteger) self.scrollView.contentOffset.y/h;
	ul_tile_x = MAX(MIN(ul_tile_x, cols-2), 0);
	ul_tile_y = MAX(MIN(ul_tile_y, rows-2), 0);
	
	// Reuse and retask as appropriate
	TileView* newTileViews[4] = {nil, nil, nil, nil};	// 0=00=UL, 1=01=UR, 2=10=BL, 3=11=BR
	TileView* oldTileViews[4]; [tiles getObjects:oldTileViews range:NSMakeRange(0, 4)];
	
	// Reuse tiles, as possible
	for (NSInteger i = 0; i <= 3; i++)
	{
		CGFloat ox	= (ul_tile_x+(i&1))*w;
		CGFloat oy	= (ul_tile_y+(i>>1))*h;
		CGRect r	= CGRectMake(ox, oy, w, h);
		
		for (NSInteger j = 0; j <= 3; j++)
		{
			if (oldTileViews[j] && CGRectEqualToRect([oldTileViews[j] frame], r))
			{
				newTileViews[i] = oldTileViews[j];
				oldTileViews[j] = nil;
				
				break;
			}
		}
	}
	
	// Retask tiles, as necessary
	for (NSInteger i = 0; i <= 3; i++)
	{
		if (newTileViews[i]) continue;
		for (NSInteger j = 0; j <= 3; j++)
		{
			if (oldTileViews[j])
			{
				CGFloat ox	= (ul_tile_x+(i&1))*w;
				CGFloat oy	= (ul_tile_y+(i>>1))*h;
				
				newTileViews[i] = oldTileViews[j];
				oldTileViews[j] = nil;
				
				newTileViews[i].frame = CGRectMake(ox, oy, w, h);
				[newTileViews[i] setNeedsDisplay];
				
				break;
			}
		}
	}
}

The approach is pretty simple:

  • Given the scroll view’s contentOffset, find the 4 tile positions that (might be) visible
  • Search for existing tiles that are already configured for one of these 4 positions
  • Reconfigure any remaining tiles for the missing positions
Share and Enjoy:
  • Twitter
  • Facebook
  • Digg
  • Reddit
  • HackerNews
  • del.icio.us
  • Google Bookmarks
  • Slashdot
This entry was posted in iPhone. Bookmark the permalink.