Cocoa’s WebViews provide a scripting object through which you can execute JavaScript from Objective-C and Objective-C from JavaScript.

Unfortunately, getting feedback from the JavaScript executed inside a WebView is not totally straight forward. Exceptions are converted into undefineds (more on that here) and you can only get back a single return value to use for debugging.

Wouldn’t it be neato if you could just continue using console.log() calls from inside JavaScript like you’re used to and have the output displayed in Xcode’s Debugger Console? Good news, folks. You can! Here’s how:

First, set a frameLoadDelegate for your WebView. I’ll just use the application delegate to keep the example simple.

#import "MyAppDelegate.h"

@implementation MyAppDelegate

@synthesize webView, scriptObject;

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    [webView setFrameLoadDelegate:self];
    [webView setMainFrameURL:@"http://jerodsanto.net"];
}

The delegate method to employ is -webView:didFinishLoadForFrame. This will be called after each frame in the WebView is loaded. Since you only want to set up the bridge once, check that the frame that was just loaded is named “_top” (more info).

- (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame {
    if (frame == [frame findFrameNamed:@"_top"]) {
        // bridge code will go here
    }
}

Once inside the if statement, the scripting environment is totally initialized. Get a reference to the scripting object:

scriptObject = [sender windowScriptObject];

Now, register your object so its methods can be called from JavaScript:

[scriptObject setValue:self forKey:@"MyApp"];

At this point, the instance of MyAppDelegate is accessible to JavaScript as window.MyApp and its methods can be called from JavaScript! Well, not just yet…

For safety reasons, you have to opt-in your Objective-C methods to be executable from JavaScript. First, add the method that will be called. It will simply take the message string from JavaScript and pass it to NSLog.

- (void)consoleLog:(NSString *)aMessage {
    NSLog(@"JSLog: %@", aMessage);
}

Okay, the method is defined. Now it has to be made explicitly available to JavaScript by implementing a class method called isSelectorExcludedFromWebScript:

+ (BOOL)isSelectorExcludedFromWebScript:(SEL)aSelector {
    if (aSelector == @selector(consoleLog:)) {
        return NO;
    }

    return YES;
}

All that is left now is to define/override the window.console object which will bridge its log function to the exposed MyAppDelegate object’s consoleLog method:

[scriptObject evaluateWebScript:@"console = { log: function(msg) { MyApp.consoleLog_(msg); } }"];

That’s all there is to it! Here is the example MyAppDelegate.m in its entirety:

#import "MyAppDelegate.h"

@implementation MyAppDelegate

@synthesize webView, scriptObject;

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    [webView setFrameLoadDelegate:self];
    [webView setMainFrameURL:@"http://jerodsanto.net"];
}

- (void)consoleLog:(NSString *)aMessage {
    NSLog(@"JSLog: %@", aMessage);
}

+ (BOOL)isSelectorExcludedFromWebScript:(SEL)aSelector {
    if (aSelector == @selector(consoleLog:)) {
        return NO;
    }

    return YES;
}

- (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame {
    if (frame == [frame findFrameNamed:@"_top"]) {
        scriptObject = [sender windowScriptObject];
        [scriptObject setValue:self forKey:@"MyApp"];
        [scriptObject evaluateWebScript:@"console = { log: function(msg) { MyApp.consoleLog_(msg); } }"];
    }
}

@end

Once you have this set up you can use console.logs to your heart’s desire and get the feedback you need right there in Xcode. If there is an easier/better way, please do let me know. Hope this helps!