Cocoaはやっぱり!
インターネットにアクセスしよう
Web Kit #5 : ページタイトルとURLを表示

今回のテーマ

今回のテーマは、「表示しているページのタイトルとURLを表示する」というものです。技術的には、WebFrameLoadDelegateというプロトコルの解説になります。

推奨環境 この解説は、以下の環境を前提に作成し、動作確認等を行っています。ご確認ください。

改版履歴

サンプルの概要

前回作成したコードレスWebブラウザでは、リンクジャンプしてもページのタイトルは表示されませんでしたし、URLの入力エリアのURLは変わりませんでした。今回は、この2点を主な改良点として解説していきます。ウィンドウは以下のようになります。前回から変わっているのは、ウィンドウの下のステータス表示(NSTextField)と経過表示(NSProgressIndicator)が追加されている点です。今回は、この2つは扱いませんが、今後の解説の中では、共通のユーザインターフェイスを使いますので、今回から追加することにしています。

インスタンス構成図

インスタンスの構成図をみておきましょう。MyWebViewDelegateというのがコーディングする部分で、WebViewの様々なデリゲートとなって、いろいろな通知を受けます。NSObjectのサブクラスとして作ります。その通知を受けて、ウィンドウ内にあるビューをコントロールしますので、それぞれについてのアウトレットを持っています。

vProgressが経過表示を行うためのNSProgressIndicator、vUrlがURLを入力するNSTextField、vReloadがリロードを行うNSButton、vStopが読み込み中止のNSButton、vTextLargeが文字を大きくするためのNSButton、vTextSmallがテキストを小さくするNSButton、vForwardが履歴を進むためのNSButton、vBackwardが履歴を戻るためのNSButton、vWebがWebコンテンツを表示するWebView、vStatusがステータスを表示するNSTextFieldです。

WebViewは、MyWebViewDelegateを様々なデリゲートとして参照しますが、これはメソッドでデリゲートインスタンスを指定するのでInterface Builderでのコネクションは必要ありません。

MyWebViewDelegate.h
#import <Foundation/Foundation.h> #import <WebKit/WebKit.h> @interface MyWebViewDelegate : NSObject { IBOutlet id vWeb; // コンテンツ表示 IBOutlet id vUrl; // URL入力 IBOutlet id vBackward; // 戻るボタン IBOutlet id vForward; // 進むボタン IBOutlet id vStop; // 中止ボタン IBOutlet id vReload; // リロードボタン IBOutlet id vTextLarge; // テキストを大きくする IBOutlet id vTextSmall; // テキストを小さくする IBOutlet id vProgress; // 経過表示 IBOUtlet id vStatus; // ステータス表示 } @end
デリゲートを設定

WebViewが沢山のデリゲートを持っていることは前回解説しました。どのインスタンスをデリゲートに割り当てるかをWebViewに知らせるために、WebViewには「 setXXXDelegate : 」というメソッドがあります。アプリケーション起動直後などのタイミングでデリゲートをMyWebViewDelegateのインスタンスにセットしておきましょう。

だたし、やみくもに全部のデリゲートをMyWebViewDelegateのインスタンスにセットしてしまうと逆にブラウザが動かなくなってしまいます。デリゲートに設定されたインスタンスは、通知を受けるだけでなく、通知に対して返事をしてWebViewの動きを指示するものもあります。指示を行う部分の処理が書いていないと、WebViewは、次の処理を行わなくなる場合があります。

最初は、Webページのタイトル表示と、URL表示の更新を行うために、WebFrameLoadDelegateのみを設定します。設定には、WebViewの setFrameLoadDelegate : メソッドを使います。

デリゲートのためのインスタンスも用意しておく必要がありますので、MyWebViewDelegateというNSObjectのサブクラスを作成して、インスタンスをInterface Builder上で作成しましょう。

デリゲートの設定: MyWebViewDelegate.m
- (void) applicationDidFinishLaunching : (NSNotification *) aNotification { // (1) デリゲートの設定 [ vWeb setFrameLoadDelegate : self ]; // (2) ユーザインターフェイスの更新 [ self updateUserInterface ]; }

MyWebViewDelegateのインスタンスは、アプリケーションのデリゲートにもしておきましょう。Interface Builder上でインスタンスを作成したら、File's Ownerのdelegateアウトレットに接続してください。そうすることで、アプリケーション起動時に applicationDidFinishLaunching : メソッドが呼ばれるようになります。

(1) setFrameLoadDelegate : メソッドで、自分自身をデリゲートに設定しています。

(2) 自前のメソッドupdateUserInterfaceを呼んでユーザインターフェイスの初期化を行います。このメソッドは以下のようになっています。

ユーザインターフェイスの更新: MyWebViewDelegate.m
- (void) updateUserInterface { // (1) 経過表示 [ vProgress setDoubleValue : [ vWeb estimatedProgress ] ]; // (2) 戻るボタン更新 [ vBackward setEnabled : [ vWeb canGoBack ] ]; // (3) 進むボタン更新 [ vForward setEnabled : [ vWeb canGoForward ] ]; }

(1) WebViewには現在の経過状況を取得するestimatedProgressというメソッドがあります。読み込みが始まった瞬間が0.0で、完了したら1.0になります。vProgressに接続しているNSProgressIndicatorは、これにあわせて最小値を0.0に、最大値を1.0にしておきます。そうすれば、取得した値をそのままsetDoubleValue : するだけで経過が表示されます。

(2) 戻るボタンは、ブラウザを起動した直後は履歴が無いので使用できません。使用できるかどうかはWebViewのcaGoBackメソッドで取得できますので、この値を使ってsetEnabled : メソッドをで制御します。

(3) 進むボタンも同様で、WebViewのcanGoForwardメソッドを使って、使用可能かを取得します。

このupdateUserInterfaceメソッドは、いろいろな場所から使用することになります。

WebFrameLoadDelegateへの通知のシーケンス

ここからが本題になります。WebFrameLoadDelegateがどのように呼ばれてくるのか、全体の流れをシーケンス図を使って説明しておきます。

何らかのアクション ( URL入力やリンククリック、戻るボタンなど ) でWebViewがページの読み込み処理を開始します。フレームについてデータの読み込みの開始の通知として、webView : didStartProvisionalLoadForFrame : メソッドが呼ばれます。この時、どのURLの読み込みかの情報も取れますので、URLの表示を更新することが出来ます。その後、WebViewはWebサーバにリクエストを送信します。

データ受信が始まって、ページタイトルが確定した時点で、webView : didReceiveTitle : forFrame : メソッドが呼ばれます。ここでタイトルの表示を更新することが出来ます。

Webページに小さいアイコンをつけることが出来ますが、それを受信したら、webView : didReceiveIcon : forFrame : メソッドが呼ばれます。ここでアイコンの表示を更新することが出来ます。

そして、ダウンロードが完了したらwebView : didFinishLoadForFrame : メソッドが呼ばれます。

これらのデリゲートメソッドの呼び出しは、フレーム毎に行われますので、例えば左右に2分割されているページ全体を読み込んでいる場合は、HTMLの数だけ ( つまり3回 ) 行われます。どのフレームに対しての通知かはパラメータで判別することができます。

読み込み開始とURLの更新

では、URLの更新のところを詳しく見ていきましょう。

読み込み開始の通知 : MyWebViewDelegate.m
- (void) webView : (WebView *) sender didStartProvisionalLoadForFrame : (WebFrame *) frame { // (1) メインフレームならURLの表示を更新 if ( frame == [ sender mainFrame ] ) { // (2) サーバへ送ったリクエストを取得 NSURLRequest *req = [ [ frame provisionalDataSource ] request ]; // (3) リクエストからURLを取得 NSString *url = [ [ req URL ] absoluteString ]; // (4) 表示を更新 [ vUrl setStringValue : url ]; [ self updateUserInterface ]; } }

フレームの読み込みが開始されると、デリゲートのwebView : didStartProvisionalLoadForFrame : メソッドが呼ばれます。この「 Provisional Load 」とは日本語にすると「 暫定的な読み込み 」ですが、これはいったい何ものでしょうか。

既に表示しているページがある場合、リンクがクリックされて次のページを読み込みを開始するわけですが、サーバからある程度の量のデータが届くまでは表示することは出来ません。その段階までは、現在のページの情報を保持しておいてスクロールなどが発生したときなどに備えます。つまり、ページジャンプの途中では「 現在表示している確定データ 」「 これから読み込もうとしている暫定データ 」の両方のデータを保持する必要があるわけですね。

図にするとこうなります。この図のように、WebFrameは2つのWebDataSourceというクラスのインスタンスを持っています。dataSourceが確定データで、provisionalDataSourceが暫定データです。暫定データが蓄積されて表示できるようになった時点で、dataSourceが破棄されて、provisionalDataSourceがdataSourceに置き換わります。

(1) さて、コードの解説に戻ります。このメソッドではURLの表示の更新を行うわけですが、フレーム分割されているページの場合、子供のフレームの中身が変わってもURLの表示は通常変えません。一番外側のフレーム ( メインフレーム ) が変わったときだけ更新しますので、その判断をまず行う必要があります。

第2パラメータは、読み込みが開始されたフレームのインスタンスです。WebViewのmainFrameメソッドを使うとメインフレームのインスタンスが取得できますので、これと比較しています。

(2) このメソッド内で欲しい情報は、暫定データのURLです。このURLはprovisionalDataSourceの中にあります。WebFrameクラスから暫定のデータをprovisionalDataSourceメソッドで取得て、さらに、サーバに送ったリクエストの内容をrequestメソッドで取り出せます。

(3) リクエスト ( NSURLRequest ) からはURLというメソッドでURLを取り出せますので、absoluteStringメソッドでURLの文字列を取得します。

(4) その文字列をsetStringValue : メソッドでNSTextFieldにセットすれば完了です。

ページタイトルの更新

続いて、ページタイトルの更新です。

タイトル受信の通知 : MyWebViewDelegate.m
- (void) webView : (WebView *) sender didReceiveTitle : (NSString *) title forFrame : (WebFrame *) frame { // (1) メインフレームならタイトルの表示を更新 if ( frame == [ sender mainFrame ] ) [ [ sender window ] setTitle : title ]; [ self updateUserInterface ]; }

(1) ページタイトルが決定したらwebView : didReceiveTitle : forFrame : メソッドが呼ばれます。これもURLの時と同じく、メインフレームかどうかを判定して、ウィンドウのタイトルをsetTitle : メソッドで変更します。タイトルは、第2パラメータにそのまま文字列として入っています。第1パラメータのsenderはWebViewですので、このWebViewが所属しているウィンドウのインスタンスは、windowメソッドで取得できます。当然ですが、WebViewもNSViewですので、NSViewのメソッドは全て使うことが出来ます。

フレームデータ読み込みの完了

フレームのデータの読み込みが完了したら、webView : didFinishLoadForFrame : メソッドが呼ばれます。フレームデータの読み込み完了というのは、HTMLのページの場合は、HTML自身と貼られている画像や子供のフレームの読み込みの完了が終わった状態を意味します。

そのため、フレーム分割の行われているWebページの場合は、内側のフレームの読み込み完了が先に通知されて、順番に外側のフレームに向かって完了通知が発生していくことになります。メインフレームが読み込み完了したときには、Webページ全体の読み込みが完了したことになります。

ただし、サブフレームのみが切り替わるようなリンクジャンプの場合は、サブフレームのみの通知が発生し、メインフレームの通知は発生しません。全てのリンクジャンプや読み込みでメインフレームの完了通知が来るわけではありませんので、そういう前提でコードを書かないように注意が必要です。Appleのドキュメントでは、フレームを使ったページを考慮していないコードで説明をしているものがいくつかあります。

読み込み完了の通知 : MyWebViewDelegate.m
- (void) webView : (WebView *) sender didFinishLoadForFrame : (WebFrame *) frame { // (1) メインフレームなら処理する if ( [ sender mainFrame ] == frame ) { // (2) タイトルがなければUntitledにする if ( [ [ [ frame dataSource ] pageTitle ] length ] == 0 ) [ [ sender window ] setTitle : @"Untitled" ]; } [ self updateUserInterface ]; }

フレームの読み込みの完了時にこのサンプルでは、タイトルを持たない画像などのコンテンツのタイトル表示を行います。

(1) メインフレームかどうかの判定をまず行います。

(2) タイトルの存在しないJPEG画像等では、「タイトルが決定したよ」のデリゲートメソッドは呼ばれません。そのため、なにもしないと直前のHTMLなどのタイトルが残ったままになってしまいます。そのため、読み込み完了のタイミングで、タイトルのないコンテンツについてはフォローを行います。

先程も出てきましたが、WebFrameの受信したコンテンツは、WebDataSourceというクラスのインスタンスの中に格納してあります。WebFrameのdataSourceメソッドでこのWebDataSourceのインスタンスが取得でき、さらにpageTitleメソッドでタイトルが取り出せます。タイトルの無いコンテンツの場合は長さが0の文字列が返ってくるのでlengthメソッドで長さを取得して判断します。0の場合は「 Untitled 」とタイトルを表示しています。