46

I need to have my iPhone Objective-C code catch Javascript errors in a UIWebView. That includes uncaught exceptions, syntax errors when loading files, undefined variable references, etc.

This is for a development environment, so it doesn't need to be SDK-kosher. In fact, it only really needs to work on the simulator.

I've already found used some of the hidden WebKit tricks to e.g. expose Obj-C objects to JS and to intercept alert popups, but this one is still eluding me.

[NOTE: after posting this I did find one way using a debugging delegate. Is there a way with lower overhead, using the error console / web inspector?]

Ned Batchelder
  • 364,293
  • 75
  • 561
  • 662
Robert Sanders
  • 720
  • 2
  • 11
  • 16

10 Answers10

28

I have now found one way using the script debugger hooks in WebView (note, NOT UIWebView). I first had to subclass UIWebView and add a method like this:

- (void)webView:(id)webView windowScriptObjectAvailable:(id)newWindowScriptObject {
    // save these goodies
    windowScriptObject = newWindowScriptObject;
    privateWebView = webView;

    if (scriptDebuggingEnabled) {
        [webView setScriptDebugDelegate:[[YourScriptDebugDelegate alloc] init]];
    }
}

Next you should create a YourScriptDebugDelegate class that contains methods like these:

// in YourScriptDebugDelegate

- (void)webView:(WebView *)webView       didParseSource:(NSString *)source
 baseLineNumber:(unsigned)lineNumber
        fromURL:(NSURL *)url
       sourceId:(int)sid
    forWebFrame:(WebFrame *)webFrame
{
    NSLog(@"NSDD: called didParseSource: sid=%d, url=%@", sid, url);
}

// some source failed to parse
- (void)webView:(WebView *)webView  failedToParseSource:(NSString *)source
 baseLineNumber:(unsigned)lineNumber
        fromURL:(NSURL *)url
      withError:(NSError *)error
    forWebFrame:(WebFrame *)webFrame
{
    NSLog(@"NSDD: called failedToParseSource: url=%@ line=%d error=%@\nsource=%@", url, lineNumber, error, source);
}

- (void)webView:(WebView *)webView   exceptionWasRaised:(WebScriptCallFrame *)frame
       sourceId:(int)sid
           line:(int)lineno
    forWebFrame:(WebFrame *)webFrame
{
    NSLog(@"NSDD: exception: sid=%d line=%d function=%@, caller=%@, exception=%@", 
          sid, lineno, [frame functionName], [frame caller], [frame exception]);
}

There is probably a large runtime impact for this, as the debug delegate can also supply methods to be called for entering and exiting a stack frame, and for executing each line of code.

See http://www.koders.com/noncode/fid7DE7ECEB052C3531743728D41A233A951C79E0AE.aspx for the Objective-C++ definition of WebScriptDebugDelegate.

Those other methods:

// just entered a stack frame (i.e. called a function, or started global scope)
- (void)webView:(WebView *)webView    didEnterCallFrame:(WebScriptCallFrame *)frame
      sourceId:(int)sid
          line:(int)lineno
   forWebFrame:(WebFrame *)webFrame;

// about to execute some code
- (void)webView:(WebView *)webView willExecuteStatement:(WebScriptCallFrame *)frame
      sourceId:(int)sid
          line:(int)lineno
   forWebFrame:(WebFrame *)webFrame;

// about to leave a stack frame (i.e. return from a function)
- (void)webView:(WebView *)webView   willLeaveCallFrame:(WebScriptCallFrame *)frame
      sourceId:(int)sid
          line:(int)lineno
   forWebFrame:(WebFrame *)webFrame;

Note that this is all hidden away in a private framework, so don't try to put this in code you submit to the App Store, and be prepared for some hackery to get it to work.

Robert Sanders
  • 720
  • 2
  • 11
  • 16
  • 1
    I tried to subclass UIWebView and add the webView:windowScriptObjectAvailable method as you described, but it never gets called. This is with the iphone 3.0 SDK. Does this not work anymore or am I doing something wrong? – pjb3 Jun 29 '09 at 16:39
  • I tried this on iphone and it works great. But, it seems that only handled exceptions are raised. It may not be helpful if unhandled exceptions passed away. – Krešimir Prcela Nov 22 '10 at 09:31
  • im using exceptionWasRaised in the delegate and iterate over frame caller. but they all have functionName returns null. Any idea? – at0mzk Feb 23 '11 at 09:25
13

I created a nice little drop-in category that you can add to your project... It is based on Robert Sanders solution. Kudos.

You can dowload it here:

UIWebView+Debug

This should make it a lot easier to debug you UIWebView :)

Pablo
  • 133
  • 1
  • 4
  • Thanks. This has been tremendously helpful, as I'm coming from MonoTouch and haven't yet gotten well-versed in Objective C. – Dan Abramov Jul 17 '12 at 18:34
  • This is [how I use your solution in MonoTouch](http://stackoverflow.com/a/11529165/458193). – Dan Abramov Jul 17 '12 at 19:06
  • Promising. However I wasn't able to open the debug url in my browser. – neoneye Aug 17 '12 at 11:21
  • I like this solution. I just wish it was easier to get an exception stack trace. At least you get a function and a line number with this. (though the line number seems offset by one - zero based?) – mpontillo Jul 01 '13 at 06:28
11

I used the great solution proposed from Robert Sanders: How can my iPhone Objective-C code get notified of Javascript errors in a UIWebView?

That hook for webkit works fine also on iPhone. Instead of standard UIWebView I allocated derived MyUIWebView. I needed also to define hidden classes inside MyWebScriptObjectDelegate.h:

@class WebView;
@class WebFrame;
@class WebScriptCallFrame;

Within the ios sdk 4.1 the function:

- (void)webView:(id)webView windowScriptObjectAvailable:(id)newWindowScriptObject 

is deprecated and instead of it I used the function:

- (void)webView:(id)sender didClearWindowObject:(id)windowObject forFrame:(WebFrame*)frame

Also, I get some annoying warnings like "NSObject may not respond -windowScriptObject" because the class interface is hidden. I ignore them and it works nice.

Community
  • 1
  • 1
Krešimir Prcela
  • 4,257
  • 33
  • 46
9

One way that works during development if you have Safari v 6+ (I'm uncertain what iOS version you need) is to use the Safari development tools and hook into the UIWebView through it.

  1. In Safari: Enable the Develop Menu (Preferences > Advanced > Show Develop menu in menu bar)
  2. Plug your phone into the computer via the cable.
  3. List item
  4. Load up the app (either through xcode or just launch it) and go to the screen you want to debug.
  5. Back in Safari, open the Develop menu, look for the name of your device in that menu (mine is called iPhone 5), should be right under User Agent.
  6. Select it and you should see a drop down of the web views currently visible in your app.
  7. If you have more than one webview on the screen you can try to tell them apart by rolling over the name of the app in the develop menu. The corresponding UIWebView will turn blue.
  8. Select the name of the app, the develop window opens and you can inspect the console. You can even issue JS commands through it.
bobc
  • 682
  • 7
  • 7
8

Straight Forward Way: Put this code on top of your controller/view that is using the UIWebView

#ifdef DEBUG
@interface DebugWebDelegate : NSObject
@end
@implementation DebugWebDelegate
@class WebView;
@class WebScriptCallFrame;
@class WebFrame;
- (void)webView:(WebView *)webView   exceptionWasRaised:(WebScriptCallFrame *)frame
       sourceId:(int)sid
           line:(int)lineno
    forWebFrame:(WebFrame *)webFrame
{
    NSLog(@"NSDD: exception: sid=%d line=%d function=%@, caller=%@, exception=%@", 
          sid, lineno, [frame functionName], [frame caller], [frame exception]);
}
@end
@interface DebugWebView : UIWebView
id windowScriptObject;
id privateWebView;
@end
@implementation DebugWebView
- (void)webView:(id)sender didClearWindowObject:(id)windowObject forFrame:(WebFrame*)frame
{
    [sender setScriptDebugDelegate:[[DebugWebDelegate alloc] init]];
}
@end
#endif

And then instantiate it like this:

#ifdef DEBUG
        myWebview = [[DebugWebView alloc] initWithFrame:frame];
#else
        myWebview = [[UIWebView alloc] initWithFrame:frame];
#endif

Using #ifdef DEBUG ensures that it doesn't go in the release build, but I would also recommend commenting it out when you're not using it since it has a performance impact. Credit goes to Robert Sanders and Prcela for the original code

Also if using ARC you may need to add "-fno-objc-arc" to prevent some build errors.

psy
  • 2,791
  • 26
  • 26
  • 2
    You can also just make a category for UIWebView with the webView:didClearWindowObject:forFrame: method instead of having to instantiate different class types. – Jeremy Flores May 26 '12 at 17:11
  • I got an error about Receiver type WebScriptCallFrame for instance message is a forward declartion. on ios6 – zztczcx Aug 27 '14 at 06:30
4

I have created an SDK kosher error reporter that includes:

  1. The error message
  2. The name of the file the error happens in
  3. The line number the error happens on
  4. The JavaScript callstack including parameters passed

It is part of the QuickConnectiPhone framework available from the sourceForge project

There is even an example application that shows how to send an error message to the Xcode terminal.

All you need to do is to surround your JavaScript code, including function definitions, etc. with try catch. It should look like this.

try{
//put your code here
}
catch(err){
    logError(err);
}

It doesn't work really well with compilation errors but works with all others. Even anonymous functions.

The development blog is here is here and includes links to the wiki, sourceForge, the google group, and twitter. Maybe this would help you out.

Jason Plank
  • 2,336
  • 5
  • 31
  • 40
0

I have done this in firmware 1.x but not 2.x. Here is the code I used in 1.x, it should at least help you on your way.

// Dismiss Javascript alerts and telephone confirms
/*- (void)alertSheet:(UIAlertSheet*)sheet buttonClicked:(int)button
{
    if (button == 1)
    {
        [sheet setContext: nil];
    }

    [sheet dismiss];
}*/

// Javascript errors and logs
- (void) webView: (WebView*)webView addMessageToConsole: (NSDictionary*)dictionary
{
    NSLog(@"Javascript log: %@", dictionary);
}

// Javascript alerts
- (void) webView: (WebView*)webView runJavaScriptAlertPanelWithMessage: (NSString*) message initiatedByFrame: (WebFrame*) frame
{
    NSLog(@"Javascript Alert: %@", message);

    UIAlertSheet *alertSheet = [[UIAlertSheet alloc] init];
    [alertSheet setTitle: @"Javascript Alert"];
    [alertSheet addButtonWithTitle: @"OK"];
    [alertSheet setBodyText:message];
    [alertSheet setDelegate: self];
    [alertSheet setContext: self];
    [alertSheet popupAlertAnimated:YES];
}
kdbdallas
  • 4,513
  • 10
  • 38
  • 53
  • I had found that somewhere, but it doesn't appear to work for me in 2.1. Assuming that those are methods added to a subclass of UIWebView. – Robert Sanders Oct 10 '08 at 22:51
0

See exception handling in iOS7: http://www.bignerdranch.com/blog/javascriptcore-example/

[context setExceptionHandler:^(JSContext *context, JSValue *value) {
    NSLog(@"%@", value);
}];
  • 1
    Hi, Yoav, welcome to Stack Overflow. You may find this useful: http://stackoverflow.com/help/how-to-answer. Please consider making your answer more useful to the readers: "Links to external resources are encouraged, but please add context around the link so your fellow users will have some idea what it is and why it’s there. Always quote the most relevant part of an important link, in case the target site is unreachable or goes permanently offline." "Brevity is acceptable, but fuller explanations are better." – Honza Zidek Feb 22 '15 at 22:48
0

First setup WebViewJavascriptBridge , then override console.error function.

In javascript

    window.originConsoleError = console.error;
    console.error = (msg) => {
        window.originConsoleError(msg);
        bridge.callHandler("sendConsoleLogToNative", {
            action:action,
            message:message
        }, null)
    };

In Objective-C

[self.bridge registerHandler:@"sendConsoleLogToNative" handler:^(id data, WVJBResponseCallback responseCallback) {
    NSString *action = data[@"action"];
    NSString *msg = data[@"message"];
    if (isStringValid(action)){
        if ([@"console.error" isEqualToString:action]){
            NSLog(@"JS error :%@",msg);
        }
    }
}];
Patrick
  • 262
  • 3
  • 12
-3

A simpler solution for some cases might be to just add Firebug Lite to the Web page.

Oleg
  • 531
  • 3
  • 8