絵で見て分かる、簡単WebKitアプリの作り方

最近WebKitがいろいろなWebブラウザで使われるようになってきています。Safariはもちろん、GoogleChromeAndroidAdobe AIRなどなど。
MacCocoaアプリケーションでは、簡単にWebKitをHTMLレンダラ(JavaScriptも対応)にとして組み込めるようになっています。そこで簡単なシングルウィンドウ(タブもなし)のWebブラウザを作ってみます。


このブラウザの用途としては、Webアプリを普通のアプリのように利用できるようにすることが挙げげられます。例えば、twicliを動かせばTwitterクライアント・アプリの出来上がり、Gmailならメーラー…などなど。


というわけで、絵で見て分かる(?)Cocoa版シングルウィンドウWebブラウザの作り方をどうぞ。なお、事前にXcodeインストールが必要です。

1. Cocoaアプリのプロジェクトを作る

Xcodeを起動し、「新規プロジェクト」を選択し、Cocoa Applicationを選びます。

そして、適当に名前をつけて保存します。

次にWebKitを利用できるようにするため、Frameworksの中で右クリックし、追加→既存のフレームワークを選び、WebKit.frameworkを追加しておきます。

2.ウィンドウを作ってWebKitを配置

次に、超簡単なユーザインターフェースを作ります。Resourcesの中からMainMenu.xibをダブルクリックして開きます。
するとInterface Builderが起動し、メインウィンドウが一つ作られた状態になっていると思います。
このメインウィンドウに、Library(Tools->Library)から、WebKitの中にある WebViewをドラッグ&ドロップで配置します。これがHTMLレンダラの役割をします。

WebViewのサイズをウィンドウ一杯に変更し、

ウィンドウのリサイズに合わせてWebViewもリサイズされるよう、インスペクタで以下の設定にしましょう。

3/22追記

インスペクタで「Plugins」と「Java」を切っておかないと、プラグインJavaを使用するサイトではアプリが落ちてしまうようです。原因追及中。きっと何かが足りないんだと思う。

3. アプリケーションコントローラを作る

このままではURLを指定していないので、真っ白のままです。
実はテキストフィールドを追加して、右ドラッグでWebViewと結ぶだけでURL欄が出来てしまうのですが、あえてここでは起動時にハードコードしたURLを開くようにします。


まずアプリケーション全体をコントロールするためのクラスを作ります。
Xcodeに戻り、新たなクラスを作ります。新規ファイルを選び、Objective-C classを作成。

適当にクラスに名前をつけて保存します。ここではAppControllerとしました。

AppController.hとAppController.mが作られ、コードの外枠が自動生成されます。
※下図ではResourcesに追加されてしまいましたが、Classesに移すべきですね。
先ほど作成したウィンドウとWebViewをコントローラから参照できるようにするため、以下のようにヘッダにIBOutletを2つ追加します。
WebViewの定義のはいったWebKitのヘッダもimportしておきます。

#import <Cocoa/Cocoa.h>
#import <WebKit/WebKit.h>

@interface AppController : NSObject {
	IBOutlet NSWindow* window;
	IBOutlet WebView* browser;
}
@end

上のコードを保存したあと、Interface Builderに戻り、Read All Headersを選びます。これでInterface Builderから先ほど作ったクラスが見えるようになります。

次にAppControllerを一つインスタンス化しましょう。LibraryからNSObjectをxibファイル内に配置します。

そしてインスペクタでクラス名を AppController に変更します。
すると、先ほど定義したIBOutletがインスペクタのOutlets一覧に出てくるはずです。

この状態で、AppControllerからWindowに向かって右ドラッグ(またはCtrl-ドラッグ)すると、線が引かれます。
マウスボタンを離すとメニューが出てくるので、windowを選びます。
これで、今作ったAppControllerのインスタンスのwindowメンバが、メインウィンドウを参照した状態になります。

同様に、AppControllerのbrowserにメインウィンドウ内のWebViewを接続しましょう。

以上が終わったら、再びXcodeに戻り、以下のコードをAppController.mの @implementation AppController 内にに追加します。

- (void) awakeFromNib
{
	[browser setMainFrameURL:@"http://twitter.com/home"];
}

awakeFromNibは、xibに配置した通りにオブジェクトがインスタンス化され、Outletsが結びつけられた状態になった直後に呼ばれるメソッドです。
ここで、ブラウザ(browserで参照できる)に特定のURLを開くよう指示するわけです。

4. 試してみる

Xcodeでコマンド-Rを叩くと、作ったアプリケーションがビルドされ、実行されます。
↓実行したところ

これだけでちゃんとリンクも機能するし、JavaScriptだって動きます。
ちなみに戻る/進む/リロードなどは、キーボード(コマンド+カーソルキー等)や右クリックで出来ますが、ボタンを作りたくなったら、Interface Builderでボタンを配置して右ドラッグでWebViewに接続し、goBack:などアクションを選ぶだけです。しかもコマンド-R一発でその場で試せます。簡単。

5. 新規ウィンドウはSafariで開くようにする

さて、いじっていると変なことに気づきます。新規ウィンドウを開くようなリンクが動作しないのです。
これはウィンドウを開くといった操作はWebViewには行えないので、アプリケーション側で処理してあげる必要があるためです。
といっても今作っているのはあくまでウィンドウ一つだけのブラウザなので、Safariなどのデフォルトブラウザに投げてしまいましょう。*1


まず、Interface Builderで、WebViewからAppControllerに向かって右ドラッグして線を引き、policyDelegateおよびUIdelegateに指定します。
これで、WebViewから必要な時にAppControllerのメソッドを呼び出してくれます(定義されている場合のみ)。

そして、AppController.mに以下のコードを追加します。delegateの場合、ヘッダは特に要りません。

- (WebView *) webView:(WebView *) sender createWebViewWithRequest:(NSURLRequest *) request
{
	NSURL *url = [[request URL] absoluteURL];
	[[NSWorkspace sharedWorkspace] openURL:url];
	return NULL;
}

- (void) webView:(WebView *)sender
		 decidePolicyForNewWindowAction:(NSDictionary *)actionInformation
		 request:(NSURLRequest *)request newFrameName:(NSString *)frameName
		 decisionListener:(id)listener
{
	NSURL *url = [[request URL] absoluteURL];
	[[NSWorkspace sharedWorkspace] openURL:url];
}

それぞれ target="_blank" の場合(UIdelegate)、target="hogehoge" と名前指定された場合(policyDelegate)に対応します。

これで新規ウィンドウを開くリンク(Twitterの発言内のURLとか)をクリックすると、デフォルトブラウザでリンクが開かれます。

6. ウィンドウを閉じたらアプリケーションも終了させる

今のままだとメインウィンドウを閉じてもアプリケーションが生き残ってしまい、手動で終了させる必要があります。
そこで、メインウィンドウを閉じたら自動的にアプリケーションも終了させましょう。


まず、Interface BuilderでメインウィンドウからAppControllerに線を引き、ウィンドウのdelegateとして登録します。

そして、Xcodeに戻り、以下のコードをAppController.mに追加します。

- (void)windowShouldClose:(NSNotification *)notification
{
	[[NSApplication sharedApplication] terminate:self];
}

windowShouldClose: はウィンドウを閉じようとした際に呼び出されるdelegateメソッドです。

7. その他おまけコード

ウィンドウを閉じた時に、ウィンドウの大きさを記録し、次回起動時に再現するようにする

なお設定は、Info.plist のBundle identifierで指定したバンドル名 + .plist というファイル名で、~/Library/Preferences に保存されます。

/* AppContoller.m */
- (void) awakeFromNib
{
	 NSUserDefaults* ud = [NSUserDefaults standardUserDefaults];
	 NSRect rect;
	 rect.origin.x = [ud integerForKey:@"x"];
	 rect.origin.y = [ud integerForKey:@"y"];
	 rect.size.height = [ud integerForKey:@"h"];
	 rect.size.width = [ud integerForKey:@"w"];

	 if (rect.size.height && rect.size.width) {
		[window setFrame:rect display:YES];
	}
	
	[browser setMainFrameURL:@"http://twitter.com/home"];
}

- (void)windowShouldClose:(NSNotification *)notification
{
	NSUserDefaults* ud = [NSUserDefaults standardUserDefaults];
	 NSRect rect = [window frame];
	[ud setInteger:rect.origin.x forKey:@"x"];
	[ud setInteger:rect.origin.y forKey:@"y"];
	[ud setInteger:rect.size.height forKey:@"h"];
	[ud setInteger:rect.size.width forKey:@"w"];
	[ud synchronize];	

	[[NSApplication sharedApplication] terminate:self];
}
ウィンドウのタイトルに、読み込んだページのタイトルを表示させる

Interface BuilderでAppContollerを、WebViewのframeLoadDelegateとしてに接続したのち、AppContoller.mに以下のコードを追加します。

- (void)webView:(WebView *)sender didReceiveTitle:(NSString *)title forFrame:(WebFrame *)frame
{
	if (![frame parentFrame])
		[window setTitle:title];
}

parentFrameがない(つまり子フレームでない)ときのみウィンドウにタイトルを反映させています。

(3/22追記) JavaScriptでダイアログを表示

alertとconfirm用。promptも本当は要りますが…

- (void)webView:(WebView *)sender runJavaScriptAlertPanelWithMessage:(NSString *)message
{
	NSRunAlertPanel(@"JavaScript alert", message, @"OK", nil, nil);
}

- (BOOL)webView:(WebView *)sender runJavaScriptConfirmPanelWithMessage:(NSString *)message
{
	int choice = NSRunAlertPanel(@"JavaScript confirm", message, @"OK", @"Cancel", nil);
	return choice == NSAlertDefaultReturn;
}


この調子で、起動時に開くページの設定インターフェースとか、一定時間自動リロード機能とか、思うがままに開発を進めていけばOK。
なお、WebViewのリファレンスは以下にあります。

http://developer.apple.com/DOCUMENTATION/Cocoa/Reference/WebKit/Classes/WebView_Class/Reference/Reference.html

*1:Safariに投げてしまってはJavaScriptの関連が切れてしまうので、複数ウィンドウを使うWebアプリは適切に動作しないことがあります。