Offline MapKit solution for iOS - ios

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.
Allow pinch/zoom of a map with definable annotations
Map must be available offline
Map should be restricted to a certain geo area
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.

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.

Try using http://mapbox.com/ which has both an iOS SDK and an app for constructing offline maps.

Check section 10.3 of the Google Maps Terms of Service and you'll find that you're not allowed to store any Google Maps content. That means that you'll need to provide not only your own map, but also replace the handy MapKit functionality. You really can't use MapKit at all for offline applications, as far as I can tell.

Unfortunately MapKit Framework doesn't support Offline Map Access. You Should prefer MapBox iOS
SDK (MapBox iOS SDK is a toolset for building maps applications which support offline caching policy, zooming limits, retina display behavior, starting coordinate, and map view dragging deceleration etc....
Find the example link
Happy Coding

See the skobbler/Telenav SDK - it's based on OpenStreetMap and is an (almost) full stack replacement to mapkit (map rendering, routing & turn by turn navigation) with integrated support for offline maps

Related

iOS / MapKit cache management issue with MKTileOverlay and PINCache library

I am using MapKit in order to create satellite and radar animation by adding MKTileOverlay over the mapView.
With an UISlider and a PlayButton I was able to create an animation, like a GIF by playing with the alpha of the MKOverlayRenderer (setting them to 0 or 0.75 according to the position of my slider).
The animation is quite smooth, all my satellite and radar tiles are loaded properly over the mapView.
I am encountering one issue with the cache management.
I realized that MapKit didn't use cache for my tileOverlay that's why I used the library PINCache in order to save my tiles so that it doesn't request and download the images each time I'm playing the animation.
My implementation :
I override the method URLForTilePath in order to build my URL to get my tile images.
- (NSURL *)URLForTilePath:(MKTileOverlayPath)path{
double latMin, latMax, longMin, longMax;
path.contentScaleFactor = 1.0;
NSMutableArray *result = [[NSMutableArray alloc] init];
result = getBBoxForCoordinates((int)path.x, (int)path.y, (int)path.z);
longMin = [[result objectAtIndex:0] doubleValue];
latMin = [[result objectAtIndex:1] doubleValue];
longMax = [[result objectAtIndex:2] doubleValue];
latMax = [[result objectAtIndex:3] doubleValue];
NSString *finalURL = self.url;
finalURL = [finalURL stringByReplacingOccurrencesOfString:#"DATE"
withString:_date];
NSString *bbox = [NSString stringWithFormat:#"bbox=%f,%f,%f,%f", longMin, latMin, longMax, latMax];
finalURL = [finalURL stringByReplacingOccurrencesOfString:#"BBOX"
withString:bbox];
return [NSURL URLWithString:finalURL];
}
And the key method that will call URLForTilePath is my implementation of loadTileAtPath :
- (void)loadTileAtPath:(MKTileOverlayPath)path
result:(void (^)(NSData *data, NSError *error))result
{
if (!result)
{
return;
}
NSString *str = self.isRadar == true ? [NSString stringWithFormat:#"Radar%#", self.date] : [NSString stringWithFormat:#"Satellite%#", self.date];
NSData *cachedData = [[PINCache sharedCache] objectForKey:str];
if (cachedData)
{
result(cachedData, nil);
}
else
{
NSURLRequest *request = [NSURLRequest requestWithURL:[self URLForTilePath:path]];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
NSString *str = self.isRadar == true ? [NSString stringWithFormat:#"Radar%#", self.date] : [NSString stringWithFormat:#"Satellite%#", self.date];
[[PINCache sharedCache] setObject:data forKey:str block:nil];
result(data, connectionError);
}];
}
}
Basically what I'm trying to achieve is :
Check if I have cached Data, if so then get the object.
If not, I make a request in order to download the tile with the URL given by URLForTilePath.
I then set the Object to the Cache.
The string str is my Key for the cache management
I have 2 important values in order to sort and differentiate the tiles, the type (Radar or Satellite, different image, different URL), and the Date.
I can see that the cache management is working, the Overlays are rendering way faster but the main issue I'm encountering is that it doesn't load and build the tiles at the according coordinate.
My mapView is like a puzzle of the world wrongly built.
With my piece of code, can you see something wrong I made ?
I couldn't find the reason to this memory/cache management issues with mapKit.
I decided to try with GoogleMap, it tooks me less than 2 hours to implement it, and the results are amazing.
I wanted to respect the Apple community by using Apple solution, but Google Map provided me so much more performances.

Is there any way to bundle map data with an iOS app?

I've got a client interested in an iOS app, but one of the requirements is an offline map. Unfortunately, both Google and iOS maps require an active internet connection to download data. Once you've opened the app up once, I think you can trigger a cache for an area, but you still require that connection.
Is there any way to bundle the actual map data with the app, or would I need to reinvent the wheel by writing my own map code?
If you app is iOS 7+, you have some flexibility by using MKTileOverlay. It allows you to put your own tile set on top of a normal Apple map.
By creating a MKTileOverlay subclass and overload loadTileAtPath: you could check your bundle for the initial state, then maybe a separate cache for tiles loaded when there was an internet connection, and finally use the URL to load the tile when all else fails.
- (void)loadTileAtPath:(MKTileOverlayPath)path
result:(void (^)(NSData *data, NSError *error))result
{
if (!result) {
return;
}
NSString *filePath = ...; // Predetermined filename based off of MKTileOverlayPath
NSData *initialData = [NSData dataWithContentsOfFile:filePath];
NSData *cachedData = [self.cache objectForKey:[self URLForTilePath:path]];
if (initialData) {
result(initialData, nil);
} else if (cachedData) {
result(cachedData, nil);
} else {
NSURLRequest *request = [NSURLRequest requestWithURL:[self URLForTilePath:path]];
[NSURLConnection sendAsynchronousRequest:request queue:self.operationQueue completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
result(data, connectionError);
}];
}
}
More info and some examples here
I got this issue before and the only way I found was by using a third party map tiles provider called MapBox. They provide an API for iOS and other platforms as well. You can even customize your own maps by using TileMill. The former even allows you to export the map tiles as an MBTiles file which provides a way of storing millions of tiles in a single SQLite database making it possible to store and transfer web maps in a single file.

IOS: use offline maps

in my app I want to use offline maps and with this maps use gpx files to have a route. I found openstreetmap to do it, but is there some better services? (the best solution with level curves)
thanks
Offline maps require a bit of experience in iOS since there aren't many projects and examples out there.
However, you got one project called Route-Me which could give you a starting point.
We used it to develop Metro Valencia Offline which successfully made it to the App Store.
I wrote a small tutorial on how to add Route-Me into your project.
Basically you can use any maps feed you may want (OpenStreetMaps is one of them and also the one we used). What you'll have to do is:
Download the map tiles ( https://metacpan.org/pod/Geo::OSM::Tiles )
Put them into a SQLite database ( http://shikii.net/blog/downloads/map2sqlite-bin.zip )
Modify Route-Me for the tiles to be fed from the database instead of from OpenStreetMaps website
You can test also: skobbler's/Telenav: http://developer.skobbler.com/support#download it's based on OpenStreetMap and they got full offline capabilities (GPX tracks navigation, map rendering, routing & rerouting)
There is also Nutiteq Maps SDK: http://developer.nutiteq.com , offline maps for iOS and Android, supports Xamarin IDE (C#) and native languages (Java, ObjectiveC, Swift). It is not so much routing and navigation (as e.g. Skobbler), but more for focused to complex maps with interactive layers (points, lines and polygons on top of maps). One advantage is that you can use your own base map sources (in-house, 3rd parties), not only OpenStreetMap what SDK itself provides.
Disclaimer: I'm developer of it.
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.

iOS App - Display images in a grid view froma list of JSON URL.

I'm storing a list of URLs in a MYSQL db table and I have been able to successfully retrieve the URLS via JSON.
However, now that I have the JSON I am not sure how to display the images in a image gallery type view in my iPhone App.
What I plan on having is a huge list of URL's to image in the database and I want to display x amount at a time and then once the user scrolls to the end I want to display x amount more. Like pinterest.
Any hints?
#import "mySQLAppViewController.h"
#interface mySQLAppViewController (){
}
- (IBAction)loadJSON:(id)sender;
#end
#implementation mySQLAppViewController
- (IBAction)loadJSON:(id)sender {
NSString *dataUrl = [NSString stringWithFormat:#"http://MYDOMAIN.com/results.php"];
NSURL *url = [NSURL URLWithString:dataUrl];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
AFJSONRequestOperation *operation =
[AFJSONRequestOperation JSONRequestOperationWithRequest:request
success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
for (int i =0 ; i < [JSON count]; i++) {
//NSString *image_url = JSON[i][#"image_url"];
//NSLog(#"%#", image_url);
//^^THIS SUCESSFULLY LOGS THE URL LINKS (I.E. HTTP://MYDOMAIN.COM/CAR.JPG)
NSURL *imageURL = [NSURL URLWithString:JSON[i][#"image_url"]];
NSData *imageData = [NSData dataWithContentsOfURL:imageURL];
UIImage *image = [UIImage imageWithData:imageData];
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
}
}
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
UIAlertView *av = [[UIAlertView alloc] initWithTitle:#"Error Retrieving Weather"
message:[NSString stringWithFormat:#"%#",error]
delegate:nil
cancelButtonTitle:#"OK" otherButtonTitles:nil];
[av show];
}];
[operation start];
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#end
Sounds like you want to look at UICollectionView. Here's a tutorial to get you started:
Beginning UICollectionView In iOS 6.
Edit: Edited after davidm's answer to be more complete: (https://stackoverflow.com/a/16918758/412339) Mark his as answer if it (UICollectionView) is the best for your solution.
iOS 6 and above:
You should really check out UICollectionView. A really good resource for this is found here: Create an iOS 6 UICollectionView Using Storyboards
iOS 5 and lower:
One thing that you can do is create a custom ViewController that has an array of your image URLs or your JSON Array:
#property (nonatomic, retain) NSArray *imageURLs;
Then create a set of reusable UIImageViews (which is another question in itself, but basically you need an array that holds custom UIImageViews with a reuse identifier, similar to a UITableViewCell).
#property (nonatomic, retain) NSArray *imageViews;
Finally, you will need a UIScrollView and UIView (to contain the UIImageViews) that watches the position and adds the imageview on the screen using a UIScrollViewDelegate method (scrollViewDidScroll: should work).
Alternatively, you can just implement a UITableViewController and Delegate with a custom UITableViewCell and let Apple's engineers take care of most of that for you, but you won't be able to put photos side by side, only stacked on top of each other. That may be what you are wanting though.

Table view with image caching and "pull to update" option

Almost every ios app now has something like "Feed" option.
Programming that usually includes fetching images from the web, caching them, handling pages, "pull to update option", etc. - all the standart stuff.
But looks like there is no standart solution for that?
I tried "three 20" - really big, complicated library with many modules. It really lacks good documentation! And it also had "slowdowns" while fetching images from cache.
Maybe I should use different small libraries for each small task separately? Like HJCache, EGO, etc.
Or is it better to write everything from scratch without any libraries?
Please give me advice on best practices here, i am really stuck now.
This one is very easy to drop in for pull to refresh.
For image loading, I wrote the following category for UIImageView:
// .h
#interface UIImageView (UIImageView_Load)
- (void)loadFrom:(NSURL *)url completion:(void (^)(UIImage *))completion;
#end
// .m
#import "UIImageView+Load.h"
#import <QuartzCore/QuartzCore.h>
#implementation UIImageView (UIImageView_Load)
- (void)loadFrom:(NSURL *)url completion:(void (^)(UIImage *))completion {
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if (data) {
self.image = [UIImage imageWithData:data];
if (completion) completion(self.image);
}
}];
}
#end
// To use it when building a cell
//...
MyModelObject *myModelObject = [self.myModel objectAtIndex:indexPath.row];
if (myModelObject.image) {
cell.imageView.image = myModelObject.image;
} else {
NSURL *url = [NSURL urlWithString:myModelObject.imageUrl];
[cell.imageView loadFrom:url completion:^(UIImage *image) {
// cache it in the model
myModelObject.image = image;
cell.imageView.image = image;
}];
}
// ...
I'm a fan of Leah Culver's Pull to Refresh library, or this STableViewController which handles pull-to-refresh as well as endless scrolling downward.
For image loading, try SDWebImage from the DailyMotion app.

Resources