UIWebView:页面真正加载完成是什么时候?

31

我需要知道当UIWebView完全加载了网页时的状态,这意味着当所有重定向完成并动态加载的内容准备好时。

我尝试注入JavaScript(查询document.readyState =='complete'),但这似乎不是非常可靠。

也许有一个私有API事件可以给我带来结果吗?


我认为你不需要私有API。请看下面的答案... - gtmtg
我还添加了一个答案,它使用了一个私有框架,但仍然可能不会让您被应用商店拒绝。无论如何,我建议仅在webViewDidFinishLoad方法无效时使用estimatedProgress方法... - gtmtg
你说过你尝试过注入JavaScript,但似乎不太可靠。根据我的经验,它是可靠的。你是在哪里/如何注入代码?你是在什么时候调用那段代码?你是如何调用document.readyState的? - Gruntcakes
1
我找到的唯一可行的解决方案是:https://dev59.com/0HI-5IYBdhLWcg3w0L_x#25620001 - sabiland
11个回答

48

我一直在寻找这个答案,从Sebastian的问题中得到了这个想法。不知何故这对我有用,也许对那些遇到这个问题的人也有用。

我给UIWebView设置了一个代理,在webViewDidFinishLoad中,通过执行Javascript来检测webview是否真正完成加载。

- (void)webViewDidFinishLoad:(UIWebView *)webView {
    if ([[webView stringByEvaluatingJavaScriptFromString:@"document.readyState"] isEqualToString:@"complete"]) {
        // UIWebView object has fully loaded.
    }
}

2
如果你的答案有效,那么它似乎是我在整个 Stack Overflow 上看到的最好的答案。但是,在这里已经有三年了,没有人投票支持这个答案,所以我有点怀疑……我会尝试一下。 - Richard Venable
1
太好了!但不要忘记在头文件中添加UIWebViewDelegate,以及在定义UIWebView的实现文件中添加[webViewImage setDelegate:self]。 - DeZigny
2
对于所有六个“OnPageFinished”调用,状态都是完成的。因此,它对我来说不起作用,无法检测到最后一个页面加载事件。 - Hugo Logmans
这段代码是错误的。它会给你帧的readyState而不是主文档,这才是你需要的。如果你使用这个,你仍然会记录多个“完成”状态。 - jasonjwwilliams
23
据我理解,webViewDidFinishLoad 只会在每次页面加载完成时被调用一次。因此,如果当 UIWebView 认为加载完成时文档的 readyState 不是 "complete",那么这个委托方法将不会再被调用。因此,在这种情况下,这个 if 代码块中的内容可以视为死代码。 - Robert Kang
显示剩余4条评论

22

我的解决方案:

- (void)webViewDidFinishLoad:(UIWebView *)webView
{
    if (!webView.isLoading) {
        // Tada
    }
}

1
这对我来说似乎非常有效。为什么要使用那些复杂的解决方案呢?难道我漏掉了什么吗? - Hilton Campbell
完美吗?有人能告诉我这比@fedry的回答更糟糕在哪里吗? - gr8scott06
我自己已经使用过那个解决方案几次了,"它只是有效而已"。它不是更糟,而是更好,因为它不涉及评估JavaScript。 - Sebastian
1
不行。isLoading 随机在 true 和 false 之间切换。 - Swindler
2
当加载大量iframe或出现错误时,UIWebView.isLoading有时不可靠。 - DawnSong
在非主线程运行时它会显示警告。解决方案是什么? - Dmitry

11

UIWebView的执行JavaScript的能力与页面是否加载无关。你可以使用stringByEvaluatingJavaScriptFromString:来在调用UIWebView加载页面之前执行JavaScript,或者在根本没有加载页面的情况下执行它。

因此,我无法理解为什么这个链接中的答案被接受:webViewDidFinishLoad: Firing too soon?,因为调用任何JavaScript并没有告诉你任何有用的信息(如果他们调用了一些有趣的JavaScript,例如监视dom,他们没有提到,但如果有的话,他们会因为其重要性而提到)。

你可以调用一些JavaScript,例如检查dom或已加载页面的状态,并将其返回给调用代码,但是如果它报告页面尚未加载,则该怎么办? - 你需要再过一段时间再次调用它,但是多久之后,何时,何地,多频繁,...轮询通常不是任何问题的好解决方案。

唯一可靠且可以掌控的方法是自己知道页面何时完全加载。在加载的页面中添加JavaScript事件监听器,让它们调用您的shouldStartLoadWithRequest:并传递一些专有URL。例如,你可以创建一个JS函数来监听window load或dom ready事件等,具体取决于你需要知道页面何时加载还是仅需知道dom何时加载等。如果网页不是您的,则可以将此JavaScript放入文件中并注入到您加载的每个页面中。

如何创建JavaScript事件监听器是标准JavaScript,与iOS无关,例如下面是检测dom何时加载的JavaScript代码,可在UIWebView中注入到正在加载的页面中,并导致调用shouldStartLoadWithRequest:(这会在dom完成加载后但在完整页面内容显示之前调用shouldStartLoadWithRequestwhen,要检测这一点,只需更改事件监听器即可)。

var script = document.createElement('script');  
script.type = 'text/javascript';  
script.text = function DOMReady() {
    document.location.href = "mydomain://DOMIsReady";
}

function addScript()
{        
    document.getElementsByTagName('head')[0].appendChild(script);
    document.addEventListener('DOMContentLoaded', DOMReady, false);
}
addScript();

1
DOMContentLoaded不会等待所有资源(例如图片)加载完成。例如,请参见http://ie.microsoft.com/TEStdrive/HTML5/DOMContentLoaded/Default.html。 - Chris Prince

5

当DOM的readyState状态为interactive时,WKWebView会调用didFinish委托方法,但对于加载了大量Javascript的网页来说,这个时间点太早了。我们需要等待complete状态。我已经创建了这样的代码:

func waitUntilFullyLoaded(_ completionHandler: @escaping () -> Void) {
    webView.evaluateJavaScript("document.readyState === 'complete'") { (evaluation, _) in
        if let fullyLoaded = evaluation as? Bool {
            if !fullyLoaded {
                DDLogInfo("Webview not fully loaded yet...")
                DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
                    self.waitUntilFullyLoaded(completionHandler)
                })
            } else {
                DDLogInfo("Webview fully loaded!")
                completionHandler()
            }
        }
    }
}

然后:

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
    DDLogInfo("Webview claims that is fully loaded")

    waitUntilFullyLoaded { [weak self] in
        guard let unwrappedSelf = self else { return }

        unwrappedSelf.activityIndicator?.stopAnimating()
        unwrappedSelf.isFullLoaded = true
    }
}

4
马丁H的回答是正确的,但仍可能会多次触发。
您需要在页面上添加一个Javascript onLoad监听器,并跟踪是否已经插入了监听器:
#define kPageLoadedFunction @"page_loaded"
#define kPageLoadedStatusVar @"page_status"
#define kPageLoadedScheme @"pageloaded"

- (NSString*) javascriptOnLoadCompletionHandler {
    return [NSString stringWithFormat:@"var %1$@ = function() {window.location.href = '%2$@:' + window.location.href; window.%3$@ = 'loaded'}; \
    \
    if (window.attachEvent) {window.attachEvent('onload', %1$@);} \
    else if (window.addEventListener) {window.addEventListener('load', %1$@, false);} \
    else {document.addEventListener('load', %1$@, false);}",kPageLoadedFunction,kPageLoadedScheme,kPageLoadedStatusVar];
}

- (BOOL) javascriptPageLoaded:(UIWebView*)browser {
    NSString* page_status = [browser stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"window.%@",kPageLoadedStatusVar]];
    return [page_status isEqualToString:@"loaded"];
}


- (void)webViewDidFinishLoad:(UIWebView *)browser {

    if(!_js_load_indicator_inserted && ![self javascriptPageLoaded:browser]) {
        _js_load_indicator_inserted = YES;
        [browser stringByEvaluatingJavaScriptFromString:[self javascriptOnLoadCompletionHandler]];
    } else
        _js_load_indicator_inserted = NO;
}

当页面加载完成后,JavaScript 监听器将会触发一个新的页面加载事件,该事件包含 URL 信息:pageloaded:<已完成加载的 URL>。接下来,您只需要更新 shouldStartLoadWithRequest 的代码以便能够检测到该 URL 协议即可。
- (BOOL)webView:(UIWebView *)browser shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    if([request.URL.scheme isEqualToString:kPageLoadedScheme]){
        _js_load_indicator_inserted = NO;
        NSString* loaded_url = [request.URL absoluteString];
        loaded_url = [loaded_url substringFromIndex:[loaded_url rangeOfString:@":"].location+1];

        <DO STUFF THAT SHOULD HAPPEN WHEN THE PAGE LOAD FINISHES.>

        return NO;
    }
}

这对我很有效。我进行了一些调整以处理非HTML页面。我发现,如果Web视图正在加载PDF等非HTML文档,则webViewDidFinishLoad:仅被调用一次,并且在webViewDidFinishLoad:中安装Javascript加载完成处理程序太晚了,无法触发page_loaded函数。我需要在webViewDidStartLoad:中安装加载完成处理程序Javascript,这样当webViewDidFinishLoad:第一次(也是唯一一次)被调用时,page_loaded已经存在并且可以触发哨兵页面加载。 - user2067021
_js_load_indicator_inserted 是在哪里定义和设置的?我没有在答案中看到它,也不确定如何在没有它的情况下使用它。 - kraftydevil
目前我定义如下:BOOL _js_load_indicator_inserted = NO;,但是当调用webView:shouldStartLoadWithRequest:navigationType:时,request.URL.scheme从未等于kPageLoadedScheme(@"pageloaded")。 - kraftydevil

3

是的,我尝试过那个方法,它确实做到了我想要的。虽然我不太喜欢使用这些hackish的方法来收集像这样的普通信息。或者网页的下载状态(我通过私有API获取,就像你在第二个链接中描述的那样)。 - Sebastian
我的iOS应用程序因使用“_documentView”API(需要获取estimatedProgress)而被拒绝。 - Tsuneo Yoshioka
@TsuneoYoshioka 是的 - 正如我在我的答案中所说,_documentView API 是私有的,使用它将导致您的应用被拒绝。 - gtmtg
我不知道这个怎么成为了被接受的答案。正确的答案是来自Martin H. - Gruntcakes

1
感谢JavaScript,可以不断加载新资源和新图形 - 对于足够复杂的页面,确定何时加载“完成”将相当于解决停机问题。你真的无法知道页面何时加载完成。
如果可以的话,只需显示页面或其图像,并使用webViewDidLoad更新图像,让用户决定何时截断 - 或者不截断。

有没有关于这个踩的提示?还是有人在为一个错误的答案辩护? - Neal

1
让您的视图控制器成为UIWebView的代理。 (您可以在Interface Builder中执行此操作,也可以使用以下代码在viewDidLoad方法中执行:)
[self.myWebView setDelegate:self];

然后使用此处接受的答案中描述的方法:webViewDidFinishLoad: Firing too soon?

代码:

-(void) webViewDidFinishLoad:(UIWebView *)webView
{
    NSString *javaScript = @"function myFunction(){return 1+1;}";
    [webView stringByEvaluatingJavaScriptFromString:javaScript];

    //Has fully loaded, do whatever you want here
}

我认为那不是我要找的东西。你后来的回答解决了问题 :) - Sebastian
我不认为这会起作用。 <script> 标签不是有效的 Javascript,只有标签内的内容才是。 - Scott Persinger
没有起作用。我在我的HTML中有4个iframe,这导致了4次。 - Patrick Bassut

0

这是@jasonjwwilliams答案的简化版本。

#define JAVASCRIPT_TEST_VARIBLE @"window.IOSVariable"
#define JAVASCRIPT_TEST_FUNCTION @"IOSFunction"
#define LOAD_COMPLETED @"loaded"

// Put this in your init method
// In Javascript, first define the function, then attach it as a listener.
_javascriptListener = [NSString stringWithFormat:
    @"function %@() {\
         %@ = '%@';\
    };\
    window.addEventListener(\"load\", %@, false);",
        JAVASCRIPT_TEST_FUNCTION, JAVASCRIPT_TEST_VARIBLE, LOAD_COMPLETED, JAVASCRIPT_TEST_FUNCTION];

// Then use this:

- (void)webViewDidFinishLoad:(UIWebView *)webView;
{
    // Only want to attach the listener and function once.
    if (_javascriptListener) {
        [_webView stringByEvaluatingJavaScriptFromString:_javascriptListener];
        _javascriptListener = nil;
    }

    if ([[webView stringByEvaluatingJavaScriptFromString:JAVASCRIPT_TEST_VARIBLE] isEqualToString:LOAD_COMPLETED]) {
        NSLog(@"UIWebView object has fully loaded.");
    }
}

实际上,在进一步测试中,我发现这种方法也不是完全准确的。有时,在我的测试中(加载cnn.com),输出“UIWebView对象已完全加载。”会出现多次!! - Chris Prince

0

由于所有这些答案都已经过时,而webViewDidFinishLoad已经被弃用,让我们来解释一下如何在使用Swift 4.2的WebKit(WebView)中确切地知道页面何时完成加载,经过了许多头痛之后。

class ViewController: UIViewController, WKNavigationDelegate {

    @IBOutlet weak var activityIndicator: UIActivityIndicatorView!

    override func viewDidLoad() {

        webView.navigationDelegate = self

    }

    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {

    // it is done loading, do something!

    }

}

当然,我还缺少其他默认代码,因此您正在添加到现有功能中。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接