The webViewDidFinishLoad message seems to be sent each time any object in the page has been loaded. Is there a way to determine that all loading of content is done?
I'm guessing that iframes cause the webViewDidStartLoad / webViewDidFinishLoad pair.
The [webView isLoading] check mentioned as an answer didn't work for me; it returned false even after the first of two webViewDidFinishLoad calls. Instead, I keep track of the loading as follows:
- (void)webViewDidStartLoad:(UIWebView *)webView {
webViewLoads_++;
}
- (void)webViewDidFinishLoad:(UIWebView *)webView {
webViewLoads_--;
if (webViewLoads_ > 0) {
return;
}
…
}
- (void)webView:(UIWebView*)webView didFailLoadWithError:(NSError*)error {
webViewLoads_--;
}
(Note this will only work if the start / finished pairs don't come serially, but in my experience so far that hasn't happened.)
Check this one, it will definitely work for you:
- (void)webViewDidFinishLoad:(UIWebView *)webView {
if ([[webView stringByEvaluatingJavaScriptFromString:#"document.readyState"] isEqualToString:#"complete"]) {
// UIWebView object has fully loaded.
}
}
Interesting, I wouldn't have thought it would work like that. Although I'm sure there are other ways to do it (is there a way to extract the URL from the webViewDidFinishLoad message so that you can see which one is the main page finishing loading?), the main thing I can think of is using the estimatedProgress to check the progress of the page and fire off whatever you want to do when it's 100% finished loading, which is what I do in my app. Google "iphone webview estimatedprogress" and click the first link for a guide I wrote on how to do this.
Update:
Please use phopkins' answer below instead of mine! Using private APIs in your apps is a bad idea and you will probably get rejected, and his solution is the right one.
Another way to monitor load progress with less control is to observe the WebViewProgressEstimateChangedNotification, WebViewProgressFinishedNotification, and WebViewProgressStartedNotification notifications. For example, you could observe these notifications to implement a simple progress indicator in your application. You update the progress indicator by invoking the estimatedProgress method to get an estimate of the amount of content that is currently loaded.
from http://developer.apple.com/library/mac/#documentation/Cocoa/Reference/WebKit/Classes/WebView_Class/Reference/Reference.html
You can also use this short Category I wrote that adds blocks into UIWebView and also lets you choose between default UIWebView behavior (getting notified after each object load), or the "expected" behavior (getting notified only when the page has fully loaded).
https://github.com/freak4pc/UIWebView-Blocks
I needed to capture a variable from the page which was not fully loaded yet.
This worked for me:
- (void) webViewDidFinishLoad:(UIWebView *)webView {
NSString *valueID = [webView stringByEvaluatingJavaScriptFromString:#"document.valueID;"];
if([valueID isEqualToString:#""]){
[webView reload];
return;
}
//page loaded
}
All of the options did not really work for my use case. I used phopkins example slightly modified. I found that if the HTML loaded into the webview contained an image that would be a separate request that happened serially so we have to account for that and I did that with a timer. Not the best solution but it seems to work.:
- (void)webViewActuallyFinished {
_webViewLoads--;
if (_webViewLoads > 0) {
return;
}
//Actually done loading
}
- (void)webViewDidStartLoad:(UIWebView *)webView {
_webViewLoads++;
}
- (void)webViewDidFinishLoad:(UIWebView *)webView {
[NSTimer scheduledTimerWithTimeInterval:2 target:self selector:#selector(webViewActuallyFinished) userInfo:nil repeats:NO];
}
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error {
_webViewLoads--;
}
hi it may be a bit far back already but i hope this helps.
i just used a notification when it enters the webViewDidFinishLoad method so that i can capture one instance of the method and then i'll detect the notification and do my logic from there.
hope this helps. it does not capture the last called instance of the webViewDidFinishLoad method, but allows you to do something once it has been called and not be repeated calls to that particular method (eg. showing a button) too.
*********EDIT*********
i did a test and managed to test it out. it actually works and the method called from the notification will be called after the full page has been loaded.
alternatively, i think you can do a counter on the delegate method webViewDidStartLoad and also on webViewDidFinishLoad to make sure that they are the same before you run your codes. this though, is an ugly hack as we will never know how many times it will be called unless like me, you are loading a html feed and can add a JavaScript to check how many elements there are on the page that you are loading. i'm just sharing some of the methods i have tried to solve this. hope it helps.
feedback is encouraged. thanks!
Here's what I use to show a spinner while DOM loads, built on top of #Vinod's answer.
Note that between webViewDidStartLoad and webViewDidFinishLoad the readyState is completed from the previous request (if any), that's why the polling should begin after webViewDidFinishLoad.
readyState possible values are loading, interactive or completed (and maybe nil ?)
- (void)webViewDidStartLoad:(UIWebView *)webView {
[self spinnerOn];
}
- (void)webViewDidFinishLoad:(UIWebView *)webView {
[self startDOMCompletionPolling];
}
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error {
[self startDOMCompletionPolling];
}
- (void)startDOMCompletionPolling {
if (self.domCompletionListener) {
[self.domCompletionListener invalidate];
}
self.domCompletionListener = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(checkDOMCompletion) userInfo:nil repeats:YES];
}
- (void)checkDOMCompletion {
NSString *readyState = [self.webView stringByEvaluatingJavaScriptFromString:#"document.readyState"];
if (readyState != nil) {
if (readyState.length > 0) {
if ([readyState isEqualToString:#"loading"]) { //keep polling
return;
}
}
}
//'completed', 'interactive', nil, others -> hide spinner
[self.domCompletionListener invalidate];
[self spinnerOff];
}
The way I do it is this:
- (void)webViewDidFinishLoad:(UIWebView *)webview {
if (webview.loading)
return;
// now really done loading code goes next
[logic]
}
Related
hi I want to ask a simple question how I can hide or disable progress bar when UIWebView load, I add ProgressBar as subview of webview . I did it by using this way in the method below, but it can't help me because every site take different time to load because of their content size so kindly tell me how I can hide or remove the ProgressBar when any site load in webview
- (void)makeMyProgressBarMoving {
float actual = [threadProgressView progress];
if (actual < 1) {
threadProgressView.progress = actual + 0.2;
[NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:#selector(makeMyProgressBarMoving) userInfo:nil repeats:NO];
}
else
{
threadProgressView.hidden = YES;
threadValueLabel.hidden = YES;
}
}
First add delegate to UIWebView
For adding progress bar :-
Web view delegate method :-
- (void)webViewDidStartLoad:(UIWebView *)webView
{
threadProgressView.hidden = NO;
}
For Removing progress bar :-
Web view delegate method :-
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
threadProgressView.hidden = YES;
}
Hope this helps you
Check your webview is loaded completly or not.
if(!yourWebView.loading)
{
[yourProgress removeFromSuperView];
}
loading
A Boolean value indicating whether the receiver is done loading
content. (read-only) #property(nonatomic, readonly, getter=isLoading) BOOL loading >
Discussion
If YES, the receiver is still loading content; otherwise, NO.
Availability
Available in iOS 2.0 and later.
or
You can implement the webViewDidFinishLoad delegate method of UIWebView.
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
[yourProgress removeFromSuperView];
}
webViewDidFinishLoad:
Sent after a web view finishes loading a frame.
- (void)webViewDidFinishLoad:(UIWebView *)webView
Parameters
webView
The web view has finished loading.
Availability
Available in iOS 2.0 and later.
Refer UIWebViewDelegate,UIWebView for more details
I am using facebook SDK 3.0 in my app. The delegate method is called twice when after logging to facebook.
- (void)loginViewFetchedUserInfo:(FBLoginView *)loginView
user:(id<FBGraphUser>)user {
//loginThroughFb=TRUE;
NSString *userId=[[NSString alloc] initWithString:[user id]];
[self soapCallForLogin:#"" password:#"" deviceId:#"" fbid:userId];
NSLog(#"%#",userId);
[userId release];
}
I tried 'HelloFacebookSample' project and the method is called only once.
So I guess the best solution for such case is to keep a reference to the last user object and compare it to the new object you get the next call, and if they're equal you can just ignore that call.
- (void)loginViewFetchedUserInfo:(FBLoginView *)loginView user:(id<FBGraphUser>)user {
if (![self isUser:cachedUser equalToUser:user]) {
cachedUser = user;
/// Do something
}
}
- (BOOL)isUser:(id<FBGraphUser>)firstUser equalToUser:(id<FBGraphUser>)secondUser {
return
[firstUser.objectID isEqual:secondUser.objectID] &&
[firstUser.name isEqual:secondUser.name] &&
[firstUser.first_name isEqual:secondUser.first_name] &&
[firstUser.middle_name isEqual:secondUser.middle_name] &&
[firstUser.last_name isEqual:secondUser.last_name] &&
...
}
I also had this problem. I managed to fix it with an ugly hack, but it works. I keep a counter in the FBLoginView delegate. When the fetchedUserInfo is called, I check the counter. If it is greater than zero, return. Otherwise, do two things -
1. increment the message counter
2. Fire a delayed event that zeroes the message counter again.
So your fetchedUserInfo method will look like this:
- (void)loginViewFetchedUserInfo:(FBLoginView *)loginView
user:(id<FBGraphUser>)user {
if ([self messageCounter] >0)
return;
else
{
self.messageCounter++;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC), dispatch_get_current_queue(), ^{
[self setMessageCounter:0];
});}
// Do whatever you were going to do }
Fixed in FB SDK 3.8 released on Sept 18 2013. The delegate methods are now called once per login regardless of how many times the repeated logging out and back in occur.
I was also able to reproduce this on FB SDK 3.7.1 and within their own sample program "Scrumptious"
As mentioned (at least for me) this only happens after:
Logging in once
Logging out
Logging back in (Now it happens)
What is interesting is the order of calls on re-logins:
On the first login I the calls I see are:
- (void)loginViewShowingLoggedInUser:(FBLoginView *)loginView;
- (void)loginViewFetchedUserInfo:(FBLoginView *)loginView user:(id<FBGraphUser>)user;
On the 2nd (and later) logins I see:
- (void)loginViewFetchedUserInfo:(FBLoginView *)loginView user:(id<FBGraphUser>)user;
- (void)loginViewShowingLoggedInUser:(FBLoginView *)loginView;
- (void)loginViewFetchedUserInfo:(FBLoginView *)loginView user:(id<FBGraphUser>)user;
Which gives a handy little workaround of setting a flag in the middle method like so:
- (void)loginViewShowingLoggedInUser:(FBLoginView *)loginView {
// Set flag
self.isFirstLoginDone = YES;
}
- (void)loginViewFetchedUserInfo:(FBLoginView *)loginView user:(id<FBGraphUser>)user {
// Check
if(self.isFirstLoginDone) {
// Execute code I want to run just once
NSLog(#"fetched");
}
// Don't forget to clear the flag (I guess it shouldn't matter if everything is cleaned up)
self.isFirstLoginDone = NO;
}
There could be another reason, which i jsut faced.
My situation:
ViewController A has a login (With fbloginview and its delegate set)
User chooses to register, moves to ViewController B with another fbloginview and its delegate set.
The above makes the delegate fire twice.
I have fixed this by setting delegate to nil on ViewWillDisappear in ViewController A.
-(void) viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
fbLoginButton.delegate=self;
}
-(void)viewWillDisappear:(BOOL)animated{
[super viewWillDisappear:animated];
fbLoginButton.delegate=nil;
}
I used this simple trick :
(Define an int facebookCounter in your interface)
- (void)loginViewFetchedUserInfo:(FBLoginView *)loginView
user:(id<FBGraphUser>)user {
if (self.facebookCounter==0) {
self.facebookCounter++;
return;
}
//Do stuff here
}
I needed to add thread safety in this method. A simple class variable did not work. The following two options will work, depending on the use case-
- (void)loginViewFetchedUserInfo:(FBLoginView *)loginView user:(id<FBGraphUser>)user {
//self.executedOnce = NO; in the init method of this class
#synchronized(self){
if(!self.executedOnce) {
//do something once per init of this class
self.executedOnce = YES;
}
}
//OR- This will only execute once in the lifetime of the app, thus no need for the executedOnce flag
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//do something once per lifetime of the app
});
}
just in the loginViewFetchedUserInfo method set the delegate of the loginView to nil. then it can never be called. and if you need the login again, set the delegate to the correct object.
i have web view in that i am loading pdf file .
pdf file has 2 mb size so it tacking time. i want to add indicator .
for that how can i know my file is loaded in web view ? ...
UIWebViewDelegate
in particular:
- (void)webViewDidFinishLoad:(UIWebView *)webView
James P has the right answer (^'ed), but here is the code to show and hide the indicator.
After you've registered with the UIWebViewDelegate...
- (void)webViewDidStartLoad:(UIWebView *)webView {
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
// Other stuff...
}
And then
- (void)webViewDidFinishLoad:(UIWebView *)webView {
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
// Other stuff...
}
Also you probably want to check for failures and hide the activity indicator there as well...
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error {
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
// Other stuff...
}
first Add the delegate "UIWebViewDelegate", then you can use the below methods.
– webView:shouldStartLoadWithRequest:navigationType:
– webViewDidStartLoad:
– webViewDidFinishLoad:
– webView:didFailLoadWithError:
You could pull your PDF down using ASIHTTPRequest, which would let you set up a progress bar showing real progress, and then feed it to your UIWebView once it's downloaded.
EDIT: ASIHTTPRequest is a free third-party library that provides a MUCH improved interface to iOS HTTP client functions. LOTS of code samples and instructions here: http://allseeing-i.com/ASIHTTPRequest/How-to-use
I have a webview as the detail view of a tableView navigation based app. There are "UP" and "DOWN" arrows on the navigationBar that the user can use to page through the different detail views.
Everything works fine as long as the user only clicks the "UP" or "Down" arrows once. But if the arrows are clicked multiple times and the loadRequest message is sent twice to the UIWebView, I get the error message "NSURLErrorDomain error -999" from my didFailLoadWithError method.
It seems like if a loadRequest is sent while the page is currently loading a view the error is sent. As long as the page is finished loading everything works fine.
I've tried a variety of solutions, all with the same result.
Thanks for the help!
I had the same problem today - try also this approach:
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error {
NSLog(#"didFail: %# stillLoading:%# error code:%i", [[webView request]URL], (webView.loading?#"NO":#"YES"), [error code]);
if ([error code] != -999) {
// Handle your other errors here
}
}
Reference
I solved the problem by calling:
[self.myWebView stopLoading];
myWebView.delegate = nil;
before reloading the new URL.
Before I send the new loadRequest I call:
myWebView.delegate = self;
I am able to download a ZIP file from the internet. Post processing is done in connectionDidFinishLoading and works OK except no UIView elements are updated. For example, I set statusUpdate.text = #"Uncompressing file" but that change does not appear until after connectionDidFinishLoading has completed. Similarly, the UIProgressView and UIActivityIndicatorView objects are not updated until this method ends.
Is there any way to force an update of the UIView from within this method? I tried setting [self.view setNeedsDisplay] but that didn't work. It appears to be running in the main thread. All other commands here work just fine - the only problem is updating the UI.
Thanks!
Update: here is the code that is NOT updating the UIVIEW:
-(void)viewWillAppear:(BOOL)animated {
timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(processUpdate:) userInfo:nil repeats:YES];
downloadComplete = NO;
statusText.text = #"";
}
-(void)processUpdate:(NSTimer *)theTimer {
if (! downloadComplete) {
return;
}
[timer invalidate];
statusText.text = #"Processing update file.";
progress.progress = 0.0;
totalFiles = [newFiles count];
for (id fileName in newFiles) {
count++;
progress.progress = (float)count / (float)totalFiles;
// ... process code goes here ...
}
}
At then end of processUpdate, I set downloadComplete = YES. This builds & runs without errors and works as intended except nothing updates in the UIVIEW until after processUpdate completes, then everything updates at once.
Thanks for your help so far!
As Niels said, you must return control to the run loop if you want to see views update. But don't start detaching new threads unless you really need to. I recommend this approach:
- (void)connectionDidFinishLoading:(NSConnection *)connection {
statusUpdate.text = #"Uncompressing file";
[self performSelector:#selector(doUncompress) withObject:nil afterDelay:0];
}
- (void)doUncompress {
// Do work in 100 ms chunks
BOOL isFinished = NO;
NSDate *breakTime = [NSDate dateWithTimeIntervalSinceNow:100];
while (!isFinished && [breakTime timeIntervalSinceNow] > 0) {
// do some work
}
if (! isFinished) {
statusUpdate.text = // here you could update with % complete
// better yet, update a progress bar
[self performSelector:#selector(doUncompress) withObject:nil afterDelay:0];
} else {
statusUpdate.text = #"Done!";
// clean up
}
}
The basic idea is that you do work in small chunks. You return from your method to allow the run loop to execute periodically. The calls to performSelector: will ensure that control eventually comes back to your object.
Note that a risk of doing this is that a user could press a button or interact with the UI in some way that you might not expect. It may be helpful to call UIApplication's beginIgnoringInteractionEvents to ignore input while you're working... unless you want to be really nice and offer a cancel button that sets a flag that you check in your doUncompress method...
You could also try running the run loop yourself, calling [[NSRunLoop currentRunLoop] runUntilDate:...] every so often, but I've never tried that in my own code.
While you are in connectionDidFinishLoading nothing else happens in the application run loop. Control needs to be passed back to the run loop so it can orchestrate the UI updating.
Just flag the data transfer as complete and the views for updating. Defer any heavy processing of the downloaded data to it's own thread.
The application will call your views back letting them refresh their contents later in the run loop. Implement drawRect on your own custom views as appropriate.
If you're receiving connectionDidFinishLoading in the main thread, you're out of luck. Unless you return from this method, nothing will be refreshed in the UI.
On the other hand, if you run the connection in a separate thread, then you can safely update the UI using the following code:
UIProgressView *prog = ... <your progress view reference> ...
[prog performSelectorOnMainThread:#selector(setProgress:)
withObject:[NSNumber numberWithFloat:0.5f]
waitUntilDone:NO];
Be careful not to update the UI from a non-main thread - always use the performSelectorOnMainThread method!
Do exactly what you're doing with the timer, just dispatch your processing code to a new thread with ConnectionDidFinish:. Timers can update the UI since they're run from the main thread.
The problem turned out to that the UI isn't updated in a for() loop. See the answer in this thread for a simple solution!