I'm primarily a web developer and I've been given the last minute task of throwing together an iOS application for a conference we're hosting.
We have the web app for the conference built and it is hosted on our web server. Following the tutorial here I've managed to make a very simple application which will display display the content in a UIWebView.
Our other developer has built something for android which will cache the pages and will check a .txt file on the server to see if the content has changed. If the content has changed, new content will be fetched. (conference schedule is very subject to change).
I would like to implement a similar functionality in my iOS application. Is there a simple way to go about doing this?
This is what my ViewController.m looks like:
#import "ViewController.h"
#interface ViewController ()
#end
#implementation ViewController
#synthesize appView;
- (void)viewDidLoad
{
[super viewDidLoad];
NSURL *path = [NSURL URLWithString:[NSString stringWithFormat:#"http://ict.uiuc.edu/railroad/GLXS/app/index.html"]];
NSURLRequest *request = [NSURLRequest requestWithURL:path];
[appView loadRequest:request];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#end
If i am not wrong the only thing u need to perform is to get web view refreshed when an edit is perform on remote .txt file. in my view You can download the file whenever your app is run for the first time and later on set timer that will check remote file attributes in an interval(say 1min or one sec).
Here is sample code that will trigger and return true if file is modified.
//here is sample code that will return true if file is modified
//url:your remote file url. filePathe: local file that you downloade when you app is run for first time
+ (BOOL)isServerURL:(NSString *)url NewerThanLocalFile:(NSString *)filePath {
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
NSDate *date = [attributes fileModificationDate];
NSString *lastModifiedString = nil;
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString: url] cachePolicy:NSURLCacheStorageAllowed timeoutInterval:2];
[request setHTTPMethod:#"HEAD"];
NSHTTPURLResponse *response;
[NSURLConnection sendSynchronousRequest:request returningResponse:&response error: NULL];
if ([response respondsToSelector:#selector(allHeaderFields)]) {
lastModifiedString = [[response allHeaderFields] objectForKey:#"Last-Modified"];
}
NSDate *lastModifiedServer = nil;
#try {
//set time format as required
NSDateFormatter *df = [[NSDateFormatter alloc] init];
df.dateFormat = #"EEE',' dd MMM yyyy HH':'mm':'ss 'GMT'";
df.locale = [[NSLocale alloc] initWithLocaleIdentifier:#"en_US"];
df.timeZone = [NSTimeZone timeZoneWithAbbreviation:#"GMT"];
lastModifiedServer = [df dateFromString:lastModifiedString];
}
#catch (NSException * e) {
NSLog(#"Error parsing last modified date: %# - %#", lastModifiedString, [e description]);
}
if ([date laterDate:lastModifiedServer] == lastModifiedServer)
{
return YES;
} else {
return NO;
}
} else {
return YES;
}
}
If timer returns YES refresh your web-view.Hope i am clear.
You've essentially created a very simple hybrid application, i.e. one that's part native code and part web content. There are a number of frameworks out there that make this kind of thing a lot easier. The best known is PhoneGap, the open source version of which is Apache Cordova. You can use Cordova as it comes to build your app, or you can look at the source and use the same techniques in your own code.
Since your question seems to relate to caching content, you might look into HTML5's application cache, which should work fine with or without PhoneGap.
Have a look at How to cache images and html files in PhoneGap and Download images and save locally on iPhone Phonegap app for more information.
You can also check periodically if .txt file on server is changed if so then reload the website using following code
[appView reload];
or
you can put a refresh button on top of the webview in a corner and you can check it on demand
Related
I'm an iOS newb (.NET professional), so this may be a simple issue but I couldn't find anything through the SO search or Google (and maybe not looking for the right terms).
I'm writing an app that displays information from a DD-WRT router through it's web interface. I have no problem displaying the initial page and navigating through any of the other pages, but if I make any change on a form (and it redirects to apply.cgi or applyuser.cgi), the UIWebView is blank - it's supposed to display the same page, with the form submission changes. The site works fine in Mobile Safari, which I find intriguing, but I guess UIWebView isn't totally the same.
I think the iOS code is pretty standard for display a webpage, but I'll list it below. I can't give you access to my router because, well, that's not a good idea :) Hopefully someone with a DD-WRT router can help (or know what my issue is anyway).
- (void)viewDidLoad
{
[super viewDidLoad];
NSString *sURL = #"http://user:pass#XXX.XXX.X.X";
NSURL *url = [NSURL URLWithString:sURL];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[self.webView loadRequest:request];
self.webView.delegate = self ;
}
And I'm doing a few things with Javascript in the webViewDidFinishLoad method, but I know that's not the culprit because it still happens when I comment it out.
Well I figured out the problem on my own. I think part of it was putting the username & password in the URL (which was just a temporary measure) because I found that method provided the same results in mobile Safari and desktop Chrome.
So I added MKNetworkKit to my project that provided a simple way to add authentication to my request, and found I had to make a specific request to POST the data, then reloaded the page the to see the changes.
In the (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType method, I check if ([request.HTTPMethod isEqualToString:#"POST"]) and do this:
NSString *sPostData = [[NSString alloc] initWithData:request.HTTPBody encoding:NSUTF8StringEncoding];
NSArray *aPostData = [sPostData componentsSeparatedByString:#"&"];
NSMutableDictionary *dPostData = [[NSMutableDictionary alloc] init];
//i don't know if this is the best way to set a dictionary, but it works
for (id apd in aPostData)
{
NSString *key = [apd componentsSeparatedByString:#"="][0];
NSString *val = [apd componentsSeparatedByString:#"="][1];
[dPostData setValue:val forKey:key];
}
MKNetworkEngine *engine = [[MKNetworkEngine alloc] init];
MKNetworkOperation *op = [engine operationWithURLString:[request.URL description] params:dPostData httpMethod:#"POST"];
[op setUsername:#"myUserName" password:#"myPassword" basicAuth:YES];
self.postedRequest = TRUE; //a bool I set so, when it comes to webViewDidFinishLoad, I reload the current page
[op start]; //send POST operation
I am trying to achieve the following on iOS:
Always load local files to a UIWebView for static assets (.html, .js, etc.)
Allow an update protocol such that after some period of time we can return a different set of static assets for the same URLs
Download link for minimal example.
I have this working but it seems the NSURLCache is sometimes completely missed (reproducible) and the only reliable way to fix this has been to use nasty cachebusting tricks on the page being loaded. Another equally nasty hack is to destroy the UIWebView and create another.
As an example of this we have three versions of our webapp (red, blue and green for v1, v2 and v3 respectively):
Each screen is made up of a single HTML and JS file.
index.html
----------
<!DOCTYPE html>
<html>
<head>
</head>
<body style="background-color:#FF0000">
<h1 id="label" style="color:#FFFFFF"></h1>
<script src="app.js"></script>
</body>
</html>
app.js
------
var label = document.getElementById('label');
label.innerHTML = 'red';
Every 2 seconds the following happens:
We change what files the NSURLCache will return to make it return the different versions (note we implement this by overriding cachedResponseForRequest: rather than storeCachedResponse:forRequest:)
The UIWebView loads a dummy page "http://www.cacheddemo.co.uk/index.html"
The NSURLCache logic is implemented simply as a rotating NSMutableArray:
#implementation ExampleCache
- (id)init
{
self = [super initWithMemoryCapacity:8 * 1024 * 1024 diskCapacity:8 * 1024 * 1024 diskPath:#"webcache.db"];
if(self) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(swapCache:) name:#"swapCache" object:nil];
self.cache = [#[#{#"index.html":#"index1.html", #"app.js":#"app1.js"},
#{#"index.html":#"index2.html", #"app.js":#"app2.js"},
#{#"index.html":#"index3.html", #"app.js":#"app3.js"}] mutableCopy];
}
return self;
}
- (void)swapCache:(NSNotification *)notification
{
[self.cache addObject:self.cache[0]];
[self.cache removeObjectAtIndex:0];
}
- (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request
{
NSString *file = [[[request URL] pathComponents] lastObject];
NSString *mimeType;
if([file hasSuffix:#".html"]) {
mimeType = #"text/html";
} else if([file hasSuffix:#".js"]) {
mimeType = #"application/javascript";
}
if(mimeType) {
NSString *cachedFile = self.cache[0][file];
NSUInteger indexOfDot = [cachedFile rangeOfString:#"."].location;
NSString *path = [[NSBundle mainBundle] pathForResource:[cachedFile substringToIndex:indexOfDot] ofType:[cachedFile substringFromIndex:indexOfDot + 1] inDirectory:#"www"];
NSData *data = [NSData dataWithContentsOfFile:path];
if(data.length) {
NSLog(#"Response returned for %#", file);
NSURLResponse *urlResponse = [[NSURLResponse alloc] initWithURL:[request URL] MIMEType:mimeType expectedContentLength:data.length textEncodingName:nil];
NSCachedURLResponse *response = [[NSCachedURLResponse alloc] initWithResponse:urlResponse data:data];
return response;
}
}
NSLog(#"No response for %# - %#", file, request);
return nil;
}
The view controller logic uses GCD to reload the UIWebView after the delay:
- (void)viewDidLoad
{
[super viewDidLoad];
self.webView = [[UIWebView alloc] initWithFrame:self.view.bounds];
[self.webView setAutoresizingMask:UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth];
[self.webView setDelegate:self];
[self.view addSubview:self.webView];
[self loadContent];
}
- (void)loadContent
{
[self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:#"%#?%d", #"http://www.cacheddemo.co.uk/index.html", arc4random()]]]];
// [self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:#"http://www.cacheddemo.co.uk/index.html"]]];
double delayInSeconds = 2.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[[NSNotificationCenter defaultCenter] postNotificationName:#"swapCache" object:nil];
[self loadContent];
});
}
The part I can not understand here is that adding the query string will make the page reloading work flawlessly (loads R - G - B - R - G - ...) - this is the uncommented line:
[self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:#"%#?%d", #"http://www.cacheddemo.co.uk/index.html", arc4random()]]]];
Once we get rid of the query string the NSURLCache stops being hit other than the first request so it just stays on the R page:
[self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:#"http://www.cacheddemo.co.uk/index.html"]]];
The fact that the query string causes the NSURLCache to act as it is supposed to indicates to me that the browser cache is interfering in some way. In my head I figure the caching levels work as so:
Check browser cache
If nothing returned so far - check NSURLCache
If nothing returned so far - check proxy server cache
Finally attempt to load the remote resource
How can we disable the browser cache entirely so we can completely control caching behaviour for UIWebView. Unfortunately I do not see an option to set the Cache-Control header in NSURLCache - I already tried returning a NSHTTPURLResponse with the headers set but this seems to be ignored.
I am not sure I understand correctly, but the cache-control has to be set on server side, with something like no-cache, expires and so on, and not on iOS side.
Second, by modifying the query string i.e. www.mysite.com/page?id=whatever..., iOS and any browser think the request is not the same, if you have opened the cache itself with some db editor, you should have seen one request, which is one database entry, for each changed query.
This trick of adding a random query string is quite useful for avoiding the browser to cache javascript file.
I hope I understand correctly your question.
The browser does appear to have its own "cache" above the networking layer's cache (i.e. NSURLCache).
Not sure if setting the cache control headers will solve it but if you want to try that you can do so in your code for cachedResponseForRequest. Where you create an NSURLResponse object you can use initWithURL:statusCode:HTTPVersion:headerFields: to set the header fields (including the cache control headers). In your cache control header you can try using both no-store and no-cache (e.g. http://blog.55minutes.com/2011/10/how-to-defeat-the-browser-back-button-cache/ ). If you try this please let us know if it does or does not work.
However, in practice I think the most pragmatic and maintainable solution will be to the use a number on the URL (i.e. cache busting). BTW instead of a random number you can use a simple increment that gets reset whenever the webview is allocated.
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.
Both ForgeLogs and NSLogs aren't showing up in my Safari Web Inspector when I'm testing. Am I doing something wrong or is that intentional?
[ForgeLog d:#"Playing file at..."];
EDIT: Here's the rest of the code for context. (This time using NSLog.)
#import "audio_API.h"
static AVAudioPlayer* player = nil;
#implementation audio_API
+ (void)play:(ForgeTask*)task {
// parse the file url from the file object
ForgeFile* file = [[ForgeFile alloc] initWithFile:[task.params objectForKey:#"file"]];
NSString* fileURL = [file url];
NSLog(#"Playing file at %#", fileURL);
NSURL* url = [[NSBundle mainBundle] URLForResource:fileURL withExtension:#"m4a"];
// TESTING
//url = [[NSBundle mainBundle] URLForResource:#"seconds" withExtension:#"m4a"];
// END TESTING
NSAssert(url, #"URL is invalid.");
// create the player
NSError* error = nil;
player = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error];
if(!player)
{
NSLog(#"Error creating player: %#", error);
};
[player play];
[task success:nil];
}
ForgeLog and NSLog don't log to the web inspector console. When developing a plugin, this log output can be seen in XCode, when using a plugin as part of an app this output will appear in the Toolkit or commandline tool output.
If you want to make something appear in the web inspector from your plugin, you are probably best off communicating from native code to JavaScript data through an event: http://docs.trigger.io/en/v1.4/modules/native/javascript_events.html and use console.log in the event listener.
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