Skip navigation
This discussion is archived

CATiledLayer in a UIScrollView (for native maps)

13188 Views 6 Replies Latest reply: Oct 18, 2008 4:32 AM by tomsoft RSS
David Golightly Calculating status...
Currently Being Moderated
Sep 23, 2008 1:21 PM
It's clear that the UIWebView-hosted Google Maps is a dead end. (I can explain if needed, but look around on Google Groups and you'll find the appropriate thread.) So I'm building my own native iPhone map. My current approach involves hosting a CATiledLayer (to fetch & render tiles in the current view frame) inside a UIScrollView (for event handling purposes). Does anyone have any examples of getting this to work (or anything like it)? I have no trouble getting the CATiledLayer to pull in the correct tiles; my problem is in figuring out the interaction between views and layers. I've read through Apple's docs, but they don't seem to explain the behavior I'm seeing:

NativeMapView.m (NativeMapView subclasses UIScrollView):

#import "NativeMapView.h"
#import "TileLayerDelegate.h"
#import <QuartzCore/QuartzCore.h>

@implementation NativeMapView
@synthesize tileLayer, tileProvider, isInitialized, shouldUpdate;
@dynamic mapType, center, zoom, pixelCenter;

#pragma mark ---- Public getters and setters ----

- (MapType)mapType {
return [[self tileProvider] mapType];

- (void)setMapType:(MapType)newType {
[[self tileProvider] setMapType:newType];

[self doUpdate];

- (LatLng)center {
return center;

- (void)setCenter:(LatLng)latlng {
center = latlng;
CGPoint pxCenter = [[self tileProvider] pixelFromLatLng:latlng];

[self setContentOffset:pxCenter animated:YES];

LOG(@"setCenter doing update: %lf, %lf", [self contentOffset].x, [self contentOffset].y);
[self doUpdate];

- (CGPoint)pixelCenter {
return [self contentOffset];

- (NSInteger)zoom {
return [[self tileProvider] zoom];

- (void)setZoom:(NSInteger)zoomLevel {
LOG(@"setZoom: %d", zoomLevel);
[[self tileProvider] setZoom:zoomLevel];

CGFloat side = TILESIZE * (1 << (zoomLevel+2));
CGSize viewSize = CGSizeMake(side, side);

CGRect worldMapFrame = CGRectMake(0.0, 0.0, viewSize.width, viewSize.height);

if (isInitialized) {
[self setContentOffset:[self centerForNewFrame:worldMapFrame]];
[self setContentSize:viewSize];
[self setFrame:worldMapFrame];
[tileLayer setFrame:worldMapFrame];
LOG(@"new map size: %lf, %lf", viewSize.width, viewSize.height);
LOG(@"frame size: %lf, %lf", [self frame].size.height, [self frame].size.width);

// need to reset view so it will draw
// [tileHostView removeFromSuperview];
// [self addSubview:tileHostView];

LOG(@"setZoom doing update: %lf", side);
zoom = zoomLevel;
[self doUpdate];

- (CGRect)frame {
return [super frame];

- (void)setFrame:(CGRect)newFrame {
[super setFrame:newFrame];

LOG(@"setFrame doing update");
[self doUpdate];

#pragma mark ---- UIView overrides ----

- (id)initWithTileProvider:(id<TileProvider>)provider
andFrame:(CGRect)frame {
[self setIsInitialized:NO];
[self setShouldUpdate:YES];
[self setTileProvider:provider];

CATiledLayer *tLayer = [[CATiledLayer alloc] init];
[self setTileLayer:tLayer];
[tLayer release];

TileLayerDelegate *tileDelegate = [[TileLayerDelegate alloc] init];
[tileDelegate setMapView:self];

[self setDelegate:tileDelegate]; // UIScrollView delegate
[tileLayer setDelegate:tileDelegate];

[[self layer] addSublayer:tileLayer];

[self initWithFrame:frame];

[self setMinimumZoomScale:0.1];
[self setMaximumZoomScale:10.0];
[self setShowsVerticalScrollIndicator:NO];
[self setShowsHorizontalScrollIndicator:NO];
[self setScrollsToTop:NO];
[self setBounces:NO];

return self;

- (void)drawRect:(CGRect)rect {
LOG(@"drawRect: %lf, %lf, %lf, %lf", rect.origin.x, rect.origin.y, rect.size.width, rect.size.height);
if (isInitialized) {
[[self layer] addSublayer:tileLayer];
LOG(@"drawing rect");
[super drawRect:rect];

- (void)dealloc {
[tileLayer release];

[super dealloc];

+ (Class)layerClass {
return [CATiledLayer class];

#pragma mark ---- Map position functions ----

- (void)setCenterWithLatLng:(LatLng)latlng andZoom:(NSInteger)newZoom {
LOG(@"setCenterWithLatLng: %lf, %lf, %d",, latlng.lng, newZoom);
if ( == 0.0 && latlng.lng == 0.0) {
[self setShouldUpdate:NO];
[self setZoom:newZoom];
[self setCenter:latlng];

if (!isInitialized) {
[self setIsInitialized:YES];

[self setShouldUpdate:YES];
[self doUpdate];

- (NSString *)urlStringForPixel:(CGPoint)pixel {
return [tileProvider urlStringFromPixel:pixel];

- (NSInteger)zoomForScale:(CGFloat)scale {
CGFloat zoomDelta = log2(scale);
LOG(@"zoomDelta: %lf, %lf", zoomDelta, scale);
NSInteger zoomLevel = round(zoomDelta) + [self zoom];

LOG(@"new zoom level: %d", zoomLevel);
return zoomLevel;

- (CGPoint)centerForNewFrame:(CGRect)newFrame {
CGPoint offset = [self contentOffset];
CGSize size = [self contentSize];
CGFloat xPct = offset.x/size.width,
yPct = offset.y/size.height;
return CGPointMake(xPct * newFrame.size.width, yPct * newFrame.size.height);

- (void)doUpdate {
if (isInitialized && shouldUpdate) {
LOG(@"tileLayer: %@, %d", tileLayer, [[[self layer] sublayers] containsObject:tileLayer]);
[self setNeedsDisplay];



#import "TileLayerDelegate.h"
#import "NativeMapView.h"

@implementation TileLayerDelegate
@synthesize mapView;

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
LOG(@"drawLayer: %@ inContext: %@", layer, ctx);
// convert the CA coordinate system to the iPhone coordinate system
CGContextTranslateCTM(ctx, 0.0f, 0.0f);
CGContextScaleCTM(ctx, 1.0f, -1.0f);

CGRect box = CGContextGetClipBoundingBox(ctx);

// invert the Y-coord to translate between CA coords and iPhone coords
CGPoint pixelTopLeft = PointByScalingPoint(box.origin, CGPointMake(1.0f, -1.0f));
LOG(@"pixelTopLeft: %lf, %lf", pixelTopLeft.x, pixelTopLeft.y);
NSString *tileUrlString = [mapView urlStringForPixel:pixelTopLeft];

LOG(@"url: %@", tileUrlString);
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:tileUrlString]];
UIImage *image = [[UIImage alloc] initWithData:data];

CGContextDrawImage(ctx, box, [image CGImage]);
[image release];

- (void)dealloc {
[mapView release];
[super dealloc];


When I run this code, NativeMapView's drawRect: is called to render the tiles instead of the TileLayerDelegate's drawLayer:inContext:. The call stack indicates that drawRect gets called via UIView(CALayerDelegate) drawLayer:inContext: (the superclass's methods) rather than via the delegate I set up for the CATiledLayer in initWithTileProvider (which, I've confirmed, is called). What gives?

And more generally, does this approach even make sense?

Thanks in advance!
Mac OS X (10.5.5), iPhone SDK 2.1/Xcode 3.1
  • rustyshelf Calculating status...
    Hi David -> any luck with this? I am trying to do exactly the same thing and are having similar hassles to what you are...
    PB G4, Intel Mac Mini, Mac OS X (10.4.8)
  • CesarAlaniz Calculating status...
    Currently Being Moderated
    Sep 29, 2008 12:33 PM (in response to rustyshelf)
    Count me as the third person to attempt this. So far so good, I have code to tile my map and position it at any Lat/Lon I want This is my own view called MapView. Now I'm trying to implement the smooth scroll behavior by adding my MapView as the contentView for a vanilla UIScrollView.
    Macbook, Mac OS X (10.5.4), NA
  • tomsoft Calculating status...
    Hello David,

    also blocked with the same issue: my TiledLayer delegate is not called! But I did not see any difference between your first code sample and the second one. did you corrected it, or did i miss something?


More Like This

  • Retrieving data ...

Bookmarked By (0)


  • This solved my question - 10 points
  • This helped me - 5 points
This site contains user submitted content, comments and opinions and is for informational purposes only. Apple disclaims any and all liability for the acts, omissions and conduct of any third parties in connection with or related to your use of the site. All postings and use of the content on this site are subject to the Apple Support Communities Terms of Use.