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.
Related
I am currently trying to access a webpage where the user can login using their credentials, after entering their user and password - if correct it will redirect to a new url. This new url loads a webpage with a single string which I intend to use.
However, how am I able to check the contents of the redirected url? At the moment I am only able to check the Response/Data/Contents of the initial page loaded by the following method;
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
casSession = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
NSString *urlAddress = #"https://originalurl.com";
NSURL *httpUrl = [NSURL URLWithString:urlAddress];
NSURLRequest *requestObj = [NSURLRequest requestWithURL:httpUrl];
[loginPage loadRequest:requestObj];
NSURLSessionDataTask *redirect = [casSession dataTaskWithURL:httpUrl completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSString *newURL = [NSString stringWithFormat: #"%#", response.URL];
if ([newURL containsString:#"ticket=ST"]) {
NSString * registrationID = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(#"REGISTRATION: %#", registrationID);
if (registrationID != nil) {
dispatch_async(dispatch_get_main_queue(), ^{
loginPage.hidden = YES;
});
}
} else {
NSLog(#"No ticket recieved");
}
}];
[redirect resume];
I'm not sure which delegate to use, in order to actively check every time a redirection happens and then obtain the contents of the new url?
Thanks.
You’re looking at this the wrong way. You should query the user for the login info directly and insert that into a single NSURLDataTask. Then the data task should query the server with the login info, and return some data.
This all happens with APIs (in a broad manner of speaking) where you will not present HTML contents to the user, but instead some sort of encoded data that is returned.
So for example, once you have a task defined from a URL or URLRequest, and you begin the task, you then use the completion handler to verify the returned data and/or error. If here, you may decode the returned data as a NSString, and then convert the JSON to objects, such as a user’s profile’s data (name, age, email, ...)
I did not go into detail in this answer because it is a very very broad topic, with many use cases. Look up some tutorials on NSURLDataTasks or consuming APIs from Swift and/or Objective-C.
I've got a quiz app on the Apple app store. The questions are stored in a Plist file. What I'm looking to do is find a way to update the Plist file via downloading a new version and not having to submit an update every time I have new questions to add
Does anyone know of a decent tutorial which may help me?
Many thanks.
I'm not sure about tutorials, but the steps to achieve what you describe are pretty simple:
Create a url request to your remote data
Parse the returned data
Write the parsed data to a new local plist
Eg:
// Create a NSURL to your data
NSString *dataPath = #"www.mydata.com/path";
NSURL *dataURL = [NSURL URLWithString:dataPath];
// Create an asycnhronous request along with a block to be executed when complete
[NSURLConnection sendAsynchronousRequest:[[NSURLRequest alloc] initWithURL:dataURL]
queue:[[NSOperationQueue alloc] init]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
if (error) {
NSLog(#"%#", error.localizedDescription);
// Handle the request error
}
else {
// We have some data, now we need to serialize it so that it's usable
NSError *serializationError;
NSDictionary *serializedDictionary = [NSJSONSerialization JSONObjectWithData:data options:0 error:&serializationError];
if (serializationError) {
NSLog(#"%#", serializationError.localizedDescription);
// Handle the serialization error
}
else {
// We have a serialized NSDictionary, now we just want to write it
// First we create the path to write it to, eg. uers documents directory
NSURL *documentsDirectory = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
// Then write it
BOOL success = [serializedDictionary writeToFile:documentsDirectory.path atomically:YES];
if (!success) {
NSLog(#"Error writing file");
}
}
}
}];
Note: You may want to think about storing your data in a remote database like Parse. This way you can query for new questions and only download those so you're not using unnecessary bandwidth. You may also want to consider using Core Data to maintain your local data rather than writing it to a plist. The main advantage being that you don't have to serialise the entire plist into memory to use it - you can just query for the particular questions you need.
Hope this helps.
An app that I am making will contain mass amounts of images. So obviously it would be really bad to store them all on the users phone and take up all the disk space. As I am new to iOS Development, how would I best go about loading an image remotely into a UIImageView. What are my options?
Just to let you know I would only ever be loading a maximum of 2 images at a time, if it helps.
Any advise would be very much appreciated. Thank you in advance!
You can download image Asynchronously by unblocking the main thread using the below code
NSURLRequest* request=[NSURLRequest requestWithURL:[NSURL URLWithString:#"Your imageURL"]];
[NSURLConnection sendAsynchronousRequest:request queue:[[NSOperationQueue alloc]init] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
if (error==nil && [data length]>0)
{
UIImage* img=[UIImage imageWithData:data];
// set the downloaded image to imageview here
}
else
{
NSLog(#"%#",error);
}
});
Use SDImageView or similar open sources, another options if you are loading images in table, use Lazy Loading by Apple
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.
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