36

I know when its done loading... (webViewDidFinishLoad), but I want to use

[webView.layer renderInContext:UIGraphicsGetCurrentContext()];

to create an image from the UIWebView. Occasionally I get the image prior to the webView finishing its rendering. I can use performSelector to delay the get of the image, but the amount of wait is arbitrary and brittle.

tillerstarr
  • 2,616
  • 1
  • 21
  • 35
  • I'm having this exact same issue. I do a renderInContext on webViewDidFinishLoading and it renders the previous state of the web view to the context. The odd thing is that the first time I load content in the web view, the screenshot is rendered properly. It's only after the first content load that it starts to exhibit the above behavior – Adam Ritenauer Nov 09 '11 at 18:58
  • 1
    I don't think there's a good way of doing this. I tried injecting some javascript via stringByEvaluatingJavaScriptFromString to call back out to my Obj-C code but it wasn't any more reliable than an arbitrary delay. :-( – EricS Nov 13 '11 at 04:42
  • 1
    I concur. I do a delayed performSelector as tillerstarr describes in the question, which is a waste of time and gives inconsistent results. I happen to run jQuery in my UIWebView, and it has a "ready" event that fires and is supposed to be more reliable that a regular Javascript onLoad. I guess in theory, one could have the "ready" event trigger some type of link activity that would fire webView:shouldStartLoadWithRequest:navigationType. Do you think this is worth trying? – Rob Reuss Dec 13 '11 at 04:16

6 Answers6

4

This may depend upon the kind of graphics context you need the view rendered into, but you can call

- (void)drawRect:(CGRect)area forViewPrintFormatter:(UIViewPrintFormatter *)formatter

which apparently tricks the UIWebView into thinking that it's being printed. This may help if your ultimate goal is to capture the complete page. We've had the problem recently in which even if the page was fully loaded, calling plain old -drawRect: didn't render the entire page if some of it was offscreen.

Sean Reilly
  • 748
  • 6
  • 17
2

I had a similar problem in an application where I fully control the content. This won't work if you load arbitrary pages from the web but it may work if you know your high-level DOM structure and it doesn't change much.

What worked for me was the following.

I had to recursively find the first UIWebView's subview of type UIWebOverflowScrollView with a frame of (0, 0, 1024, 768). If I couldn't find one, that was a sure sign the content hasn't been rendered yet. Having found it, I check this view's layer.sublayers.count. When the rendering finishes, I would always end up with one or three sublayers. If the rendering hasn't finished, however, the content view always had at most one sublayer.

Now, that's specific to my DOM structure—but it may be possible that you make up a similar “test” if you compare sublayer tree before and after rendering. For me, the rule of thumb was “the first recursively found subview of type UIWebOverflowScrollView will have at least three sublayers”, for you it will likely be different.

Anyway, take great care if you decide to use this approach, for even though you won't get rejected for looking into UIWebView's view and layer hierarchy, this kind of behaviour is unreliable and is very likely to change in future versions of iOS. It may very well be inconsistent between iOS 5 and iOS 6 as well.

Finally, code snippets for MonoTouch which should be straightforward to translate to Objective C:

bool IsContentScrollView (UIScrollView scrollView)
{
    return scrollView.Frame.Equals (new RectangleF (0, 0, 1024, 768));
}

[DllImport ("/usr/lib/libobjc.dylib")]
private static extern IntPtr object_getClassName (IntPtr obj);

public static string GetClassName (this UIView view) {
    return Marshal.PtrToStringAuto (object_getClassName (view.Handle));
}
bool IsWebOverflowScrollView (UIScrollView scrollView)
{
    return scrollView.GetClassName () == "UIWebOverflowScrollView";
}

IEnumerable<UIScrollView> ScrollViewsInside (UIView view)
{
    foreach (var subview in view.Subviews) {
        foreach (var scrollView in ScrollViewsInside (subview).ToList())
            yield return scrollView;

        if (subview is UIScrollView)
            yield return (UIScrollView)subview;
    }
}

bool CanMakeThumbnail {
    get {
        var scrollViews = ScrollViewsInside (this).Where (IsWebOverflowScrollView).ToList ();
        var contentView = scrollViews.FirstOrDefault (IsContentScrollView);

        // I don't know why, but this seems to be a good enough heuristic.
        // When the screen is black, either contentView is null or it has just 1 sublayer.

        // This may *break* on any iOS updates or DOM changes--be extra careful!

        if (contentView == null)
            return false;

        var contentLayer = contentView.Layer;
        if (contentLayer == null || contentLayer.Sublayers == null || contentLayer.Sublayers.Length < 3)
            return false;

        return true;
    }
}
Dan Abramov
  • 264,556
  • 84
  • 409
  • 511
2


What about using the window.onload or jQuerys $(document).ready event to trigger the shouldStartLoad callback?

Something like:

$(document).ready(function(){
    location.href = "app://do.something"
})

and in your UIWebViewDelegate do something like:

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
    NSURL *url = request.URL;

    if([url.host isEqualToString:@"app"]){
        //maybe check for "do.something"
        //at this point you know, when the DOM is finished
    }
}

With this method you can forward every possible event from the JS code to your obj-c code.

Hope that helps. The code sample is written in the browser, and therefore not tested! ;-)

devios1
  • 36,899
  • 45
  • 162
  • 260
ASP
  • 773
  • 1
  • 8
  • 22
  • I have the same problem. Your proposed solution doesn't work, the JS doesn't know when the page has actually finished rendering. – 1800 INFORMATION Oct 28 '20 at 21:17
  • @1800INFORMATION This solution worked back in 2011 when I posted this answer. Without knowing your specific issue with my proposal, I suggest you checkout the [documentation](https://api.jquery.com/ready/) of the latest version of jQuery, or try listening for the `DOMContentLoaded` event, before assigning the `href`. – ASP Oct 29 '20 at 07:17
0
- (void)webViewDidFinishLoad:(UIWebView *)webView {
   if (webView.isLoading)
        return;
    else
    {
            [self hideProgress];
    }

}
Ankit Srivastava
  • 12,347
  • 11
  • 63
  • 115
-2

If you have access to the HTML file and can edit it - then just add some special variable which will tell the code that rendering is done.
Then use method stringByEvaluatingJavaScriptFromString to know should you start some actions or not.
The example below is pretty dirty, but hope it will help and give you the idea:

(void)webViewDidFinishLoad:(UIWebView *)webView
{
    BOOL renderingDone = NO;
    while (!(renderingDone == [[webView stringByEvaluatingJavaScriptFromString:@"yourjavascriptvariable"] isEqualToString:@"YES"]))
    {
        // the app will be "freezed" till the moment when your javascript variable will not get "YES" value 
    }
    // do your stuff, rendering is done 
}
eeerahul
  • 1,629
  • 4
  • 27
  • 38
Cool
  • 5
  • 2
  • 3
    It's a bad idea to block UI thread in a pointless `while` loop. Also, it doesn't answer the question: rendering is done asynchronously since iOS 6, and so there is no certain way to know when it's done from JavaScript. – Dan Abramov Mar 17 '13 at 16:48
-4
if (webView.loading == YES)
        {
            //Load image
        }

Something like that might work.

Not My Name Car
  • 59
  • 1
  • 1
  • 8