Offline MapKit solution for iOS

viewPusher picture viewPusher · May 9, 2012 · Viewed 14.4k times · Source

Quick question for you. I have an app that I'm working on that will require the use of maps, but will not have a network connection. I have seen others ask similar questions about this, but my requirements are a slight bit different, so I wanted to post a new question.

Here are my requirements for the app.

  1. Allow pinch/zoom of a map with definable annotations
  2. Map must be available offline
  3. Map should be restricted to a certain geo area
  4. App does not have to pass Apple inspection. This is an enterprise app that will serve as a kiosk.

So, is there any framework or method of caching map tiles that anyone can suggest?

Thanks go out in advance.

Answer

Vova Kalinichenko picture Vova Kalinichenko · Aug 27, 2015

I used default map from MapKit and a subclass of MKTileOverlay to be able to save downloaded tiles and return already cached tiles without downloading them.

1) Change source for your default map from MapKit and use a subclass of MKTileOverlay (Used "open street map" here)

- (void)viewDidLoad{
    [super viewDidLoad];
    static NSString * const template = @"http://tile.openstreetmap.org/{z}/{x}/{y}.png";

    VHTileOverlay *overlay = [[VHTileOverlay alloc] initWithURLTemplate:template];
    overlay.canReplaceMapContent = YES;
    [self.mapView addOverlay:overlay level:MKOverlayLevelAboveLabels];
}

2) subclass from MKTileOverlay

@interface VHTileOverlay() // MKTileOverlay subclass
@property (nonatomic, strong) NSOperationQueue *operationQueue;
@end

@implementation VHTileOverlay

-(instancetype)initWithURLTemplate:(NSString *)URLTemplate{

    self = [super initWithURLTemplate:URLTemplate];
    if(self){
        self.directoryPath = cachePath;
        self.operationQueue = [NSOperationQueue new];
    }
    return self;
}


-(NSURL *)URLForTilePath:(MKTileOverlayPath)path {
    return [NSURL URLWithString:[NSString stringWithFormat:@"http://tile.openstreetmap.org/%ld/%ld/%ld.png", (long)path.z, (long)path.x, (long)path.y]];
}

-(void)loadTileAtPath:(MKTileOverlayPath)path
                result:(void (^)(NSData *data, NSError *error))result
{
    if (!result) {
        return;
    }

    NSString *pathToFilfe = [[self URLForTilePath:path] absoluteString];
    pathToFilfe = [pathToFilfe stringByReplacingOccurrencesOfString:@"/" withString:@"|"];
    // @"/" - those are not approriate for file's url...

    NSData *cachedData = [self loadFileWithName:pathToFilfe]; 
    if (cachedData) {
        result(cachedData, nil);
    } else {
        NSURLRequest *request = [NSURLRequest requestWithURL:[self URLForTilePath:path]];
        __block VHTileOverlay *weakSelf = self;
        [NSURLConnection sendAsynchronousRequest:request
                                           queue:self.operationQueue
                               completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
                                   NSLog(@"%@",[weakSelf URLForTilePath:path]);

                                   if(data){
                                       [self saveFileWithName:[[weakSelf URLForTilePath:path] absoluteString] imageData:data];
                                   }
                                   result(data, connectionError);
        }];
    }
}

-(NSString *)pathToImageWithName:(NSString *)fileName
{
    NSString *imageFilePath = [[OfflineMapCache sharedObject].cachePath stringByAppendingPathComponent:fileName];
    return imageFilePath;
}

- (NSData *)loadFileWithName:(NSString *)fileName
{
    NSString *imagePath = [self pathToImageWithName:fileName];
    NSData *data = [[NSData alloc] initWithContentsOfFile:imagePath];
    return data;
}

- (void)saveFileWithName:(NSString *)fileName imageData:(NSData *)imageData
{
//    fileName = [fileName stringByReplacingOccurrencesOfString:@"/" withString:@"|"];
//    NSString *imagePath = [self pathToImageWithName:fileName];
//    [imageData writeToFile:imagePath atomically:YES];
}

Uncomment "saveFileWithName" and run it on simulator. You can also add NSLog(fileName) to know where to get all tiles you need. (Simulator cache is in Users/YOU/Library/Developer/CoreSimulator/Devices/... And Library is a hidden directory)

After you cached all you need just put in your app's bundle (just like an any other image, if you want from box cached map). And tell your

- (void)loadTileAtPath:(MKTileOverlayPath)path
                result:(void (^)(NSData *data, NSError *error))result

to get tiles from bundle.

So now I can install my app, turn wi-fi off and I'll get those maps anyway.