[iOS] UIWebView コンテンツのロードとJavaScript連携

2016.02.04

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

今回は、UIWebViewについて、まとめてみました。 3種類のコンテンツ表示要領を把握するとともに、JavaScriptによる、アプリとコンテンツの双方向連携についてが中心です。

1 コンテンツの表示方法

UIWebViewにコンテンツを表示する方法は、次の3種類です。

  • loadRequest
  • loadHTMLString
  • loadData

全てのコンテンツ表示は、非同期に処理されるため、その現在の状況が必要な場合や、コンテンツ表示の完了後に処理が必要な場合などは、後に出てくる、関連プロパティやデリゲートの実装が必要になります。

(1) URL指定(loadRequest)

外部のURLを指定して、読み込むことができます。 URLWithStringで作成したNSURLを利用して、NSURLRequestオブジェクトを作成し、UIWebViewloadRequestを呼び出します。

NSURL *url = [NSURL URLWithString:@"https://dev.classmethod.jp/"];
NSURLRequest *req = [NSURLRequest requestWithURL:url];
[_webView loadRequest:req];

001

(2) ローカルのhtmlファイル(loadHTMLString)

UIWebViewloadHTMLStringを使用すると、作成したhtmlファイルをプロジェクトにコピーしてそれを表示させる事ができます。

NSString *path = [[NSBundle mainBundle] pathForResource:@"index"
ofType:@"html"];
NSURL *url = [NSURL fileURLWithPath:path];
NSError *err = nil;
NSString *s = [NSString stringWithContentsOfURL:url
encoding:NSUTF8StringEncoding
error:&err];
[_webView loadHTMLString:s baseURL:url];

003

なお、NSURLの生成後、それを先の「URL指定」と同じように、NSURLRequestにして、loadRequestすることも可能です。

NSString *path = [[NSBundle mainBundle] pathForResource:@"index"
ofType:@"html"];
NSURL *url = [NSURL fileURLWithPath:path];
NSURLRequest *req = [NSURLRequest requestWithURL:url];
[_webView loadRequest:req];

(4) データとMIMEタイプ(loadData)

UIWebViewloadDataを使用すると、NSData型にしたデータを、MIMEを指定してロードすることが可能です。 次の例は、ローカルのpdfファイルを表示しているものです。

NSString *path = [[NSBundle mainBundle] pathForResource:@"AppleDocument"
ofType:@"pdf"];
NSData *pdfData = [NSData dataWithContentsOfFile:path];
[_webView loadData:pdfData
MIMEType:@"application/pdf"
textEncodingName:@"utf-8" baseURL:nil];

002

2 コンテンツ表示に関連するプロパティとメソッド

コンテンツ表示に関連する、プロパティやメソッドには次のようなものがあります。

(1) request

@property(nonatomic, readonly, strong) NSURLRequest *request

読込む内容を示すURL(readonly) このプロパティで現在表示の対象となっているコンテンツが分かります。

(2) loading

@property(nonatomic, readonly, getter=isLoading) BOOL loading

読込み中がどうかのフラグ(readonly)

(3) stopLoading

- (void)stopLoading

すべてのwebコンテンツの読込みを停止する

先の、loadingプロパティと併用して、強制的にコンテンツ表示作業中の処理を止めるには、次のように記述します。

if (_webView.loading) {
[_webView stopLoading];
}

UIWebViewをreleaseする際は、そのdelegateを外す必要がありますが、その前に、このように、処理中の作業を強制的に停止する必要もあります。

(4) reload

- (void)reload

現在のページを再読込みする

3 履歴

履歴に関連する、プロパティやメソッドには次のようなものがあります。

(1) canGoBack

@property(nonatomic, readonly, getter=canGoBack) BOOL canGoBack

「戻る」処理が可能かどうか(readonly)

(2) canGoForward

@property(nonatomic, readonly, getter=canGoForward) BOOL canGoForward

「次へ」の処理が可能かどうか(readonly)

(3) goBack

- (void)goBack

履歴の1つ前のURLを読み込む

(4) goForward

- (void)goForward

履歴の一つ先のURLを読み込む

4 UIWebViewDelegateプロトコル

UIWebViewDelegateは、必要に応じてWebコンテンツがロードされたタイミングで介入できるように、幾つかのメソッドを定義しています。

コンテンツ表示は、すべて非同期で処理されるため、その開始や終了は、このデリゲートを使用することになります。

UIWebViewDelegateで定義されているメソッドは次の4種類です。

  • DidStartLoad
  • DidFinishLoad
  • didFailLoadWithError
  • shouldStartLoadWithRequest

(1) DidStartLoad

- (void)webViewDidStartLoad:(UIWebView *)webView

コンテンツの読込み開始時に呼ばれる

(2) DidFinishLoad

- (void)webViewDidFinishLoad:(UIWebView *)webView

読込み完了時に呼ばれる

(3) didFailLoadWithError

- (void)webView:(UIWebView *)webView
didFailLoadWithError:(NSError *)error

読込み失敗時に呼ばれる

(4) shouldStartLoadWithRequest

- (BOOL)webView:(UIWebView *)webView
shouldStartLoadWithRequest:(NSURLRequest *)request
navigationType:(UIWebViewNavigationType)navigationType

読込み開始前に呼ばれ、falseを返すと、その読込みを処理しないようにすることができます。 なお、requestは、そのコンテンツのロケーションで、navigationtypeは、読込みのきっかけとなった、ユーザの行動です。

このデリゲートは、falseを返すことで、見かけ上のUIWebViewの処理が無くなるため、画面遷移の処理をトラップするのに便利に使用されます。

5 アプリからJavaScriptの実行

(1) ドキュメントの取得

stringByEvaluatingJavaScriptFromStringを使用するとWebView上でJavaScriptを実行し、その結果を受け取ることができます。

- (NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script

次の例は、読み完了時に、JavaScriptでドキュメントのBodyタグ内のHTMLデータ(innerHTML)を出力させて、ログに表示している様子です。

- (void)webViewDidFinishLoad:(UIWebView *)webView
{
NSString *body = [webView stringByEvaluatingJavaScriptFromString:@"document.body.innerHTML"];
NSLog(@"html body: %@", body);
}

004

(2) 値の設定

先の方法と同じことですが、アプリ側からJavaScriptが実行できるということは、JavaScriptでできることは、何でもできるということになりそうです。 次の例は、idからhtml上のエレメントを検索して、アプリ側から値を設定している例です。

まずは、次のような画面を設計しました。 UIWebViewは、全画面でなく、下の部分だけとし、上部にTextFieldとボタンを配置しました。

005

続いて、つぎのhtmlを作成してUIWebViewに表示させます。

<br /><form action="cgi-bin/formmail.cgi" method="post">名前:<input id="name" name="name" size="40" type="text" />

パスワード:<input id="password" name="password" size="40" type="text" />

<input type="submit" value="送信" /><input type="reset" value="リセット" />

</form>```

最後に、アプリ側のボタンのアクションで、UITextFieldに入力された内容を、html側に書き込んでいます。

``` oc
- (IBAction)tapOkButton:(id)sender {
NSString *name = _nameTextField.text;
NSString *password = _passwordTextField.text;

[_webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"document.getElementById('name').value='%@'",name]];
[_webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"document.getElementById('password').value='%@'",password]];
}

実行例は、次のとおりです。

<a href="https://devio2023-media.developers.io/wp-content/uploads/2016/02/006.gif"><img class="alignnone size-full wp-image-181517" src="https://devio2023-media.developers.io/wp-content/uploads/2016/02/006.gif" alt="006" width="317" height="585" /></a>

<h2>6 JavaScriptからのアプリ実行</h2>

続いて、逆のパターンです。

これは、デリゲートメソッドである、<tt>shouldStartLoadWithRequest</tt>を使用して実現されています。

<tt>shouldStartLoadWithRequest</tt>は、新しいコンテンツへの遷移が行われる前に呼ばれますので、JavaScriptからは、アプリと通信したい内容をURLに仕込んで、強制的に画面遷移するようにします。アプリ側では、これをトラップし、必要なデータを受け取る訳です。。戻り値にfalseを設定しておけは、画面遷移は無効となりますので、まさに、JavaScriptとアプリとの連携にだけ使用するという訳です。

それでは、先ほどのサンプルを改造して、逆の連携を試してみます。 まずは、html側のJavaScriptを編集します。

送信ボタンを押した時に、function ok()が走るようにonClickイベントに仕込みます。 ok()の中では、各エレメントの値をgetElementByIdで取得して、それを一部としたURLを作成してドキュメントの遷移を行っています。

URLは、アプリでトラップできるように予め決め事を作っており、今回は、「app-api://で始まる」という取り決めを行っています。

結果的に作成されたURLは、「app-api://名前,パスワード」となっています。

<br /><form action="cgi-bin/formmail.cgi" method="post">名前:<input id="name" name="name" size="40" type="text" />

パスワード:<input id="password" name="password" size="40" type="text" />

<input type="button" value="送信" /><input type="reset" value="リセット" />

<script>
    function ok(){
        var name = document.getElementById("name").value
        var password = document.getElementById("password").value
        document.location = "app-api://" + name + "," + password
    }
</script>

</form>```

続いて、この画面遷移をアプリ側でトラップします。

<tt>shouldStartLoadWithRequest</tt>では、連携のキーワードである「app-api://」を見ています。
もし、ヒットしなかったら、通常の画面遷移なので、trueを返して、画面遷移の処理を継続します。

もし、ヒットしたら、待ちかねていたJavaScriptからの通信なので、ルールに従ってエンコードしデータを受け取ります。

エンコードして取得できたデータは、TextFieldのテキストとしてして設定しています。

``` oc
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {

NSString *requestString = [[request URL] absoluteString];
if ([requestString rangeOfString:@"app-api://"].location == NSNotFound){
// app-api://から始まらないので無視する
return true;
}
// 先頭の「app-api://」を除去する str = name,password
NSString *str = [requestString substringFromIndex:10];

NSArray *params = [str componentsSeparatedByString:@","];
_nameTextField.text = params[0];
_passwordTextField.text = params[1];
return false;
}

実際に動作している様子は、次のとおりです。

009

7 最後に

今回は、UIWebViewにおけるナビゲーション関連とコンテンツとのJavaScriptによる連携について、一通りまとめてみました。 まだ、この他にも細かいプロパティは幾つかありますが、ここまでで、大方の作業は可能になるのでは無いでしょうか。

UIKit Framework Reference > UIWebView Class Reference