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
Pingback: Things that were not immediately obvious to me » Blog Archive » UIScrollView (Zooming)