Bridging the Gap Between JavaScript's console.log and Cocoa's NSLog
27 Dec 2010
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 undefined
s (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.log
s 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!