Webkit and cocoa bindings.
During the C4 conference, I remember Brent Simmons (NewsGator, NetNewswire) and others talking about various interesting possible ways of using a web view in cocoa. I remember thinking about how cool it might be to build some custom UI in a web browser window using html, css, and javascript instead of using drawInRect and friends in my custom views. Wouldn't it be terribly cool if cocoa bindings would be easy to connect our cocoa data model to various bits of (say) an html form. This would allow us to sucker our web designer friends into making us a pretty skin for our app. Slick. (Not that this idea is so new... reference the iTunes store).
Jim Speth, the guy who inadvertently taught me PyObjc with his PyInjector project, has been working on a bit of code on top of WebKit that accomplishes the above by adding cocoa bindings to the DOM of an embedded web view. Originally written in PyObjC (woot!) it's a framework containing a subclass of NSTreeController and a custom WebView to allow us direct, Cocoa bindings aware, hierarchical access to the DOM. Cool stuff. Recently, he's been invited to add this functionality to WebKit itself. Good on ya, Jim! This functionality is now available in the current nightly builds of WebKit. Jim has also been kind enough to have recently posted an example app that shows us how to access and modify the width and height of images in a webview through cocoa bindings and some javascript. It requires that you download and mount the nightly dmg of WebKit to run. It's very pretty. Download it! You get to stretch Jim's face.
In my own app I would like to present the user with a fairly simple form-based web interface for entering data that I (or perhaps the power user) can then skin with custom css to allow easy export to a regular web site or blog. All of the data will live in a coredata store and I would really like the web view to seamlessly integrate with the rest of cocoa. I really don't want the app to look like a glorified web browser. With this in mind, I twisted Jim's example around to connect a slider and an NSTextField to a text input field in a WebView. It's available in my subversion repository at http://jonathansaggau.com/svn/JavaScriptBindings and is mostly Jim Speth's code.
Jim's code includes an Objective C class that fires KVC/KVO notifications in Cocoa for a corresponding javascript function. It allows us to call JSKVC.willChange(document, 'boxInfo'); from our javascript to fire the Cocoa KVC methods, which in turn allow us to use Cocoa bindings to connect everything together.
The HTML is just a common text input box that we give a unique ID so that javascript can identify it.
We need a little javascript magic now to model our text box and connect things up to the KVC notifier class. First we build a simple prototype class to define accessors for the text box value.
Now we need to get all of this into the cocoa runtime. In the WebFrameLoadDelegate of the WebView, we need to inject some javascript code. This is where things get really interesting. We're evaluating our boxinfo.js file and injecting the JSKVC class into javascript so that we can fire KVC notifications in Cocoa.
The interesting bits in the window below will be the webview, the NSTextField, and the slider. The other views are bound to various parts of the DOM.
There is a DomTreeController from Jim and the WebKit folks and a simple NSObjectController to hold the boxInfo class (from javascript)
The DomTreeController is bound to a new attribute of the WebView called mainFrameDocument. This gives us access to the whole of the web page DOM through cocoa bindings.
The NSObjectController is bound to the mainFrameDocument.boxInfo object that we defined in javascript. (Which would be document.boxInfo in JS)
All the is left is to bind both of the cocoa controls (NSTextView and the NSSlider) to the boxInfo NSObjectController (boxInfo.selection.content) and, well, Bob's your uncle (hi Jo).
Yo run the code we need to have the nightly build of WebKit mounted on our machine. We can set the following environment variables on our executable in xcode and run from there or we can just dump them in a script and allow that script to run the project. We're telling dyld to look for frameworks in the WebKit.app (more on this in a later post) and to unset the system WebKit framework path (causing dlyd to ignore the older version that's installed on our machine).
And now for my favorite screenshot.
Thanks to Jim's magic WebKit additions and cocoa bindings. The value in the NSTextField, the NSSlider and in the text input field of the WebView are all synchronized. We can even use the web page to input data.
In a couple of days, I'll revisit this topic for a short post on how to build WebKit as an embeddable framework so you can use this now in a shipping application (at 24MB, it's pretty big) and how the nightly builds of WebKit work. Stay tuned.
Jim Speth, the guy who inadvertently taught me PyObjc with his PyInjector project, has been working on a bit of code on top of WebKit that accomplishes the above by adding cocoa bindings to the DOM of an embedded web view. Originally written in PyObjC (woot!) it's a framework containing a subclass of NSTreeController and a custom WebView to allow us direct, Cocoa bindings aware, hierarchical access to the DOM. Cool stuff. Recently, he's been invited to add this functionality to WebKit itself. Good on ya, Jim! This functionality is now available in the current nightly builds of WebKit. Jim has also been kind enough to have recently posted an example app that shows us how to access and modify the width and height of images in a webview through cocoa bindings and some javascript. It requires that you download and mount the nightly dmg of WebKit to run. It's very pretty. Download it! You get to stretch Jim's face.
In my own app I would like to present the user with a fairly simple form-based web interface for entering data that I (or perhaps the power user) can then skin with custom css to allow easy export to a regular web site or blog. All of the data will live in a coredata store and I would really like the web view to seamlessly integrate with the rest of cocoa. I really don't want the app to look like a glorified web browser. With this in mind, I twisted Jim's example around to connect a slider and an NSTextField to a text input field in a WebView. It's available in my subversion repository at http://jonathansaggau.com/svn/JavaScriptBindings and is mostly Jim Speth's code.
Jim's code includes an Objective C class that fires KVC/KVO notifications in Cocoa for a corresponding javascript function. It allows us to call JSKVC.willChange(document, 'boxInfo'); from our javascript to fire the Cocoa KVC methods, which in turn allow us to use Cocoa bindings to connect everything together.
@implementation JSKVC
+ (BOOL)isSelectorExcludedFromWebScript:(SEL)aSelector
{
if (aSelector == @selector(object:willChangeValueForKey:))
return NO;
if (aSelector == @selector(object:didChangeValueForKey:))
return NO;
return YES;
}
+ (NSString *)webScriptNameForSelector:(SEL)aSelector
{
if (aSelector == @selector(object:willChangeValueForKey:))
return @"willChange";
if (aSelector == @selector(object:didChangeValueForKey:))
return @"didChange";
return nil;
}
- (void)object:(id)object willChangeValueForKey:(NSString *)key
{
[object willChangeValueForKey:key];
}
- (void)object:(id)object didChangeValueForKey:(NSString *)key
{
[object didChangeValueForKey:key];
}
The HTML is just a common text input box that we give a unique ID so that javascript can identify it.
<html>
<head>
<title>bindings example</title>
<script type="text/javascript" language="javascript" src="boxinfo.js"></script>
</head>
<body>
<p><input type="text" id="box" value="0" maxlength="4" name="contentBox" /></p>
</body>
</html>
</pre>
We need a little javascript magic now to model our text box and connect things up to the KVC notifier class. First we build a simple prototype class to define accessors for the text box value.
Then we'll need a method that will call our cocoa KVO willChange and didChange methods. JSKVC will get injected into the javascript runtime from cocoa below.
// bindings.js BoxInfo.prototype = { get content() { if (!this.box) return null; return this.box.value; }, set content(w) { if (!this.box) return; this.box.value = w; }, }
function BoxInfo() {
this.box = null;
this.setBox = function(newBox) {
// store the new box value, with KVO notifcations
JSKVC.willChange(document, 'boxInfo');
this.box = newBox;
JSKVC.didChange(document, 'boxInfo');
}
}
To finish things off in javascript, we need to tell it which box to deal with. Jim's code has a nice bit of functionality that allows the user to choose what he/she is modifying by clicking on an image. Mine is simply getting the box we're interested in through its unique ID.
function installBoxInfo() {
// create an BoxInfo object for the (DOM) document, using KVO notifications
JSKVC.willChange(document, 'boxInfo');
document.boxInfo = new BoxInfo();
JSKVC.didChange(document, 'boxInfo');
var boxElement = document.getElementById( 'box' );
document.boxInfo.setBox(boxElement);
//boxElement.setAttribute('style', 'outline: -3px solid white;');
boxElement.onblur = function() {
JSKVC.willChange(document, 'boxInfo');
JSKVC.didChange(document, 'boxInfo');
};
}
The boxElement.onblur event (for those of us, like me who aren't familiar with javascript) is the event that gets triggered when the user leaves the text box. Since the user is likely to have changed the contents of the text box, we call willChange and didChange to let cocoa know it needs to update the other widgets and doodads. More on this in a bit.
Now we need to get all of this into the cocoa runtime. In the WebFrameLoadDelegate of the WebView, we need to inject some javascript code. This is where things get really interesting. We're evaluating our boxinfo.js file and injecting the JSKVC class into javascript so that we can fire KVC notifications in Cocoa.
- (void)webView:(WebView *)sender windowScriptObjectAvailable:(WebScriptObject *)windowScriptObject
{
// as soon as the window object is available, inject the code for the boxInfo object
NSString *stringForBoxInfo = [self stringForResource:@"boxinfo" ofType:@"js"];
[windowScriptObject evaluateWebScript:stringForBoxInfo];
// add the key-value notification object to the script environment
[windowScriptObject setValue:[[JSKVC alloc] init] forKey:@"JSKVC"];
}
Then, once the web frame is loaded, we need to setup our BoxInfo object by calling the installBoxInfo(); function
- (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame
{
// modify the DOM document each time a new page loads
[[webView windowScriptObject] evaluateWebScript:@"installBoxInfo();"];
}
And, should we ever want to (say) call a method in javascript from cocoa, we set up a WebScriptObject by evaluating a small bit of javascript code corresponding to the object we want to message (document.boxInfo below) and then call- (id)callWebScriptMethod:(NSString *)name withArguments:(NSArray *)args;
WebScriptObject *boxInfo = [[webView windowScriptObject] evaluateWebScript:@"document.boxInfo"]; [boxInfo callWebScriptMethod:@"setBox" withArguments:[NSArray arrayWithObject:obj]];That's about all of the code. Now let's take a look at the NIB file.
The interesting bits in the window below will be the webview, the NSTextField, and the slider. The other views are bound to various parts of the DOM.
There is a DomTreeController from Jim and the WebKit folks and a simple NSObjectController to hold the boxInfo class (from javascript)
The DomTreeController is bound to a new attribute of the WebView called mainFrameDocument. This gives us access to the whole of the web page DOM through cocoa bindings.
The NSObjectController is bound to the mainFrameDocument.boxInfo object that we defined in javascript. (Which would be document.boxInfo in JS)
All the is left is to bind both of the cocoa controls (NSTextView and the NSSlider) to the boxInfo NSObjectController (boxInfo.selection.content) and, well, Bob's your uncle (hi Jo).
Yo run the code we need to have the nightly build of WebKit mounted on our machine. We can set the following environment variables on our executable in xcode and run from there or we can just dump them in a script and allow that script to run the project. We're telling dyld to look for frameworks in the WebKit.app (more on this in a later post) and to unset the system WebKit framework path (causing dlyd to ignore the older version that's installed on our machine).
#!/bin/tcsh set PATH=`dirname $0` setenv DYLD_FRAMEWORK_PATH /Volumes/WebKit/WebKit.app/Contents/Resources/ setenv WebKit_UNSET_DYLD_FRAMEWORK_PATH YES $PATH/build/debug/JavaScriptBindings.app/Contents/MacOS/JavaScriptBindings
And now for my favorite screenshot.
Thanks to Jim's magic WebKit additions and cocoa bindings. The value in the NSTextField, the NSSlider and in the text input field of the WebView are all synchronized. We can even use the web page to input data.
In a couple of days, I'll revisit this topic for a short post on how to build WebKit as an embeddable framework so you can use this now in a shipping application (at 24MB, it's pretty big) and how the nightly builds of WebKit work. Stay tuned.
Comments
KebKit?
Posted by: Mark Rowe | March 8, 2007 02:58 AM
Yes. All the Kool Kids spell everything with a K, man. Then again, perhaps trying to be Konsistent with my Kapitalization of WebKit with a big find / replace should have been done, I don't know, more than ten seconds before I hit post in marsedit...
Posted by: jonathan | March 8, 2007 03:31 AM