24

I have a simple iOS native app that loads a single UIWebView. I would like the webView to show an error message if the app doesn't COMPLETELY finish loading the initial page in the webView within 20 seconds.

I load my URL for the webView within my viewDidLoad like this (simplified):

[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.example.com"] cachePolicy:NSURLCacheStorageAllowed timeoutInterval:20.0]];

The timeoutInterval within the code above does not actually "do" anything, as Apple has it set within the OS to not actually time out for 240 seconds.

I have my webView didFailLoadWithError actions set, but if the user HAS a network connection, this never gets called. The webView just continues to try loading with my networkActivityIndicator spinning.

Is there a way to set a timeout for the webView?

adamdehaven
  • 5,890
  • 10
  • 61
  • 84
  • 1
    somebody here said the timeout interval is for connection. Once it connected to server, it won't throw error anymore. You need to implement your own NSTimer after it connected in webViewDidStartLoad and cancel by yourself http://stackoverflow.com/questions/1883888/nsmutableurlrequest-timeout-doesnt-trigger-if-data-starts-to-load-but-not-webvi – mask8 Jul 23 '12 at 15:45
  • That looks like exactly what I'm looking for... could you maybe give me some guidance on how to implement it? (I'm an Objective-C newby) – adamdehaven Jul 23 '12 at 15:48
  • I just need to know what to put for this: `-(void)webView: NSTimer * theTimer { NSLog(@"Me is here at 1 minute delay"); }` Like which target method would I use? – adamdehaven Jul 23 '12 at 15:58
  • I just wrote a very basic code that you may want to start from :) hope it helps – mask8 Jul 23 '12 at 16:03

4 Answers4

41

The timeoutInterval is for connection. Once webview connected to the URL, you'll need to start NSTimer and do your own timeout handling. Something like:

// define NSTimer *timer; somewhere in your class

- (void)cancelWeb
{
    NSLog(@"didn't finish loading within 20 sec");
    // do anything error
}

- (void)webViewDidFinishLoad:(UIWebView *)webView
{
    [timer invalidate];
}

- (void)webViewDidStartLoad:(UIWebView *)webView
{
    // webView connected
    timer = [NSTimer scheduledTimerWithTimeInterval:20.0 target:self selector:@selector(cancelWeb) userInfo:nil repeats:NO];
}
mask8
  • 3,560
  • 25
  • 34
  • I added the methods above, but it doesn't do anything after 20 seconds? - I put in a UIAlertView inside of the `cancelWeb` property but it never fires. – adamdehaven Jul 23 '12 at 16:14
  • 1
    sorry one change, not `NSTimer timerWithTimeInterval` but `scheduledTimerWithTimeInterval`. Once your webView connected to the URL, `webViewDidStartLoad` will be invoked. If not, you didn't set delegate correctly. Then that method starts timer to call `cancelWeb` method in 20 seconds. – mask8 Jul 23 '12 at 16:21
  • Thanks so much!!! Works perfectly :) One more question - do I need to do anything to release the timer if they exit the app in the `viewWillDisappear` or `viewDidUnload`? – adamdehaven Jul 23 '12 at 16:25
  • ...and one more question kind of off-topic: With the `NSLog` - how do you view things like that. For example, if I leave that there, where do I view the "log" ? – adamdehaven Jul 23 '12 at 16:27
  • ...crap, and one MORE question - is there a way to set this timer to run whenever they click a link in the UIWebView? For example, since my app is just a webView, if the user clicks a link (on the web) can we start the Timer to run so that it will show the error if the page doesn't load? – adamdehaven Jul 23 '12 at 16:31
  • I believe NSTimer will release by itself when it's invalidated in loop, or without loop it fires method then invalidates by itself. Output of NSLog can be viewed in xocde output pane. not sure if you can see it in device log or somewhere else. – mask8 Jul 23 '12 at 16:34
  • If you can respond to the comment right above your last one (starts with "...crap") you'd be amazing. Thanks! lol – adamdehaven Jul 23 '12 at 16:35
  • 1
    you can just skip first webViewDidStartLoad by using some kind of flag variable. when user clicks a link in the first page, the webView will start and call webViewDidStartLoad again each time. You need to determine if it's first load or later loads by users' click. Or you can insert js to handle user's click, but maybe that's too much. I hope this answers to your question – mask8 Jul 23 '12 at 16:37
  • I don't think `webViewDidStartLoad` is called once the app has loaded the webView initially on app launch. Is there something that IS called whenever the UIWebView page is changed or refreshed? i.e. -> Whenever the user clicks on a link and loads a new page within the webView – adamdehaven Jul 23 '12 at 16:43
  • you may want to try shouldStartLoadWithRequest http://stackoverflow.com/questions/4679378/iphone-uiwebview-get-the-url-of-the-link-clicked I believe webViewDidStartLoad is called whenever webView connected newly. But if you are not loading anything on app launch, it won't be called – mask8 Jul 23 '12 at 16:47
  • So then `shouldStartLoadWithRequest` is called every time the user clicks a link (not an app button - I mean an actual link on a webpage within the webView) ? – adamdehaven Jul 23 '12 at 16:55
  • I think so. or even if the page has iframe, I think that will be called. You can make sure by actually writing and running code :) – mask8 Jul 23 '12 at 17:05
  • I tried putting an alert in that function if the user clicks a link, but it doesn't get called. – adamdehaven Jul 23 '12 at 17:53
  • how did you implement that? I'm getting each time I click links. `- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType` – mask8 Jul 23 '12 at 18:48
  • I got the alert to work (I had declared my timer in the class twice.) The problem I'm having now is, I'm starting the timer in the BOOL you just described above. So when I click a link from the homepage, it starts a 20 second timer. I want the timer to cancel `[timer invalidate];` after that clicked link loads, but it's not canceling. I have `[timer invalidate];` in the webViewDidFinishLoad but it's not canceling the timer after the link click...? – adamdehaven Jul 23 '12 at 19:02
  • So I guess my question is - What is the "opposite" of `shouldStartLoadWithRequest` so that I can disable the timer? – adamdehaven Jul 23 '12 at 19:05
  • Here's my newest question explained (using your code above) http://stackoverflow.com/questions/11619017/ios-app-what-is-the-opposite-of-shouldstartloadwithrequest – adamdehaven Jul 23 '12 at 19:18
  • Once timer in invalidated in webViewDidFinishLoad, it doesn't call selector anymore. I have confirmed that. Possible case you might have is you are creating multiple scheduled timers at once and assign everything to one `NSTimer *` variable. more over, it's not good idea to run multiple timers in one thread. If that's the case, you gotta figure out the solution – mask8 Jul 23 '12 at 19:23
  • It doesn't call which selector anymore? So you're saying that after I invalidate the timer the first time, I can't use it and then invalidate it again? – adamdehaven Jul 23 '12 at 19:26
  • timer is created each time you call `scheduledTimerWithTimeInterval`. and each timer is invalidated by `invalidate` method. Once you call invalidate, THE timer won't call selector method. but you can create new one again by scheduledTimerWithTimeInterval. – mask8 Jul 23 '12 at 19:33
  • But if you overwrite `NSTimer *timer` before you invalidate previously created timer, then you will lose the access to the previous timer and never be able to invalidate it. that's what I meant – mask8 Jul 23 '12 at 19:33
  • Ok, so if I call `scheduledTimerWithTimeInterval` in my `shouldStartLoadWithRequest`, it should create a NEW timer (using the same `NSTimer *timer` variable) whenever a link is clicked, and if I call `[timer invalidate]; in my `webViewDidFinishLoad` it should invalidate the timer, so that a new one can be created when I click the next link, correct? (Because if Yes, then I'm pretty sure that's what I have but it's not working..? and thanks for being patient with me). The `webViewDidFinishLoad` isn't even removing the network activity indicator. – adamdehaven Jul 23 '12 at 19:36
  • So then how would I have a new timer each time a link is clicked? and then cancel that timer when that linked page loads? Because when I go to the next page (forgetting the timer for a sec) my network activity indicator does not disappear either. – adamdehaven Jul 23 '12 at 19:44
  • I would set `timer=nil` when invalidate it. And invalidate it in both `webViewDidFinishLoad` and `shouldStartLoadWithReqest` before you create new timer and only when it's nil. with this, timer is always checking 20 sec timeout on only the last request. does it make sense? – mask8 Jul 23 '12 at 23:56
  • Yes that makes sense. The problem I'm having I've come to realize (after logging almost every action in the app) is stemming from webViewDidStartLoad and webViewDidFinishLoad not being called after the secondary shouldStartLoadWithRequest. I click a link off the main page, shouldStartLoadWithRequest is called, my network activity indicator starts, and the app goes to the next page, but those two methods are never called. Why? – adamdehaven Jul 24 '12 at 00:50
  • Are you sure? Once you return TRUE in `shouldStartLoadWithRequest`, I'm getting both `webViewDidStartLoad` and `webViewDidFinishLoad` properly. There must be some code that unsets delegate of UIWebView or something.. – mask8 Jul 24 '12 at 17:41
  • I figured out what my problem is, but not how to solve it - My site in the webView is based on jQuery Mobile. It navigates via ajax, so to the webView, it doesn't look like the page actually changes once the initial page loads, therefore it never calls the `webViewDidStartLoad` or `webViewDidFinishLoad`. No idea how to fix it. The only workaround I can find is disabling the ajax navigation in jQuery mobile, but then I lose my page transitions and preloading pages into the DOM. :( – adamdehaven Jul 24 '12 at 18:14
  • 1
    oh I see. that sucks. you probably need to communicate b/w your code and js code on jquery-mobile. I would give a shot with stringByEvaluatingJavaScriptFromString:, or this might work http://stackoverflow.com/questions/5671742/send-a-notification-from-javascript-in-uiwebview-to-objectivec . I even consider phonegap or some kind of webView base framework maybe.. sorry I can't help that much – mask8 Jul 24 '12 at 18:46
  • Is there simply a way to "tell" the app that the webViewDidFinishLoad? – adamdehaven Jul 24 '12 at 18:50
  • no, the page I posted in the last comment says there is no official way. You may google a little about it. but as far as I know, there is no simple way to do it. (there seem to be a few people having the same issue with Ajax load, but couldn't find easy solution) – mask8 Jul 24 '12 at 18:54
  • This is my workaround: http://stackoverflow.com/questions/11637151/ios-app-jquery-mobile-site-include-js-file-only-if-page-is-viewed-in-native/11637291#comment15414478_11637291 Can you help? – adamdehaven Jul 24 '12 at 18:55
  • I'm experimenting a "similar" issue, but this time is with stringByEvaluatingJavaScriptFromString, which is taking long time. Could you please check this question? thanks! http://stackoverflow.com/q/28389612/4085636 – Cris R Feb 08 '15 at 04:51
8

All of the suggested solutions are not ideal. The correct way to handle this is using the timeoutInterval on the NSMutableURLRequest itself:

NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://web.site"]];

request.timeoutInterval = 10;

[webview loadRequest:request];
Sandy Chapman
  • 11,133
  • 3
  • 58
  • 67
3

My way is similar to accepted answer but just stopLoading when time out and control in didFailLoadWithError.

- (void)timeout{
    if ([self.webView isLoading]) {
        [self.webView stopLoading];//fire in didFailLoadWithError
    }
}

- (void)webViewDidStartLoad:(UIWebView *)webView{
    self.timer = [NSTimer scheduledTimerWithTimeInterval:10 target:self selector:@selector(timeout) userInfo:nil repeats:NO];
}

- (void)webViewDidFinishLoad:(UIWebView *)webView{
    [self.timer invalidate];
}

- (void)webView:(UIWebView *)webView didFailLoadWithError:(nullable NSError *)error{
    //Error 999 fire when stopLoading
    [self.timer invalidate];//invalidate for other errors, not time out. 
}
LE SANG
  • 10,955
  • 7
  • 59
  • 78
  • This is the better answer since it actually stops the web view after the timeout. I would also suggest setting `self.timer` to `nil` after you invalidate it and after it completes. – rmaddy May 02 '17 at 23:30
3

Swift coders can do it as follow:

var timeOut: NSTimer!

   func webViewDidStartLoad(webView: UIWebView) {
    self.timeOut = Timer.scheduledTimer(timeInterval: 7.0, target: self, selector: Selector(("cancelWeb")), userInfo: nil, repeats: false)
}

func webViewDidFinishLoad(webView: UIWebView) {
    self.timeOut.invalidate()
}

func webView(webView: UIWebView, didFailLoadWithError error: NSError?) {
    self.timeOut.invalidate()
}

func cancelWeb() {
    print("cancelWeb")
}
Krunal Nagvadia
  • 1,083
  • 2
  • 12
  • 33
Codetard
  • 2,441
  • 28
  • 34