[iOS] UIWebView コンテンツのロードとJavaScript連携
はじめに
今回は、UIWebViewについて、まとめてみました。 3種類のコンテンツ表示要領を把握するとともに、JavaScriptによる、アプリとコンテンツの双方向連携についてが中心です。
1 コンテンツの表示方法
UIWebViewにコンテンツを表示する方法は、次の3種類です。
- loadRequest
- loadHTMLString
- loadData
全てのコンテンツ表示は、非同期に処理されるため、その現在の状況が必要な場合や、コンテンツ表示の完了後に処理が必要な場合などは、後に出てくる、関連プロパティやデリゲートの実装が必要になります。
(1) URL指定(loadRequest)
外部のURLを指定して、読み込むことができます。 URLWithStringで作成したNSURLを利用して、NSURLRequestオブジェクトを作成し、UIWebViewのloadRequestを呼び出します。
NSURL *url = [NSURL URLWithString:@"https://dev.classmethod.jp/"]; NSURLRequest *req = [NSURLRequest requestWithURL:url]; [_webView loadRequest:req];
(2) ローカルのhtmlファイル(loadHTMLString)
UIWebViewのloadHTMLStringを使用すると、作成した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];
なお、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)
UIWebViewのloadDataを使用すると、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];
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); }
(2) 値の設定
先の方法と同じことですが、アプリ側からJavaScriptが実行できるということは、JavaScriptでできることは、何でもできるということになりそうです。 次の例は、idからhtml上のエレメントを検索して、アプリ側から値を設定している例です。
まずは、次のような画面を設計しました。 UIWebViewは、全画面でなく、下の部分だけとし、上部にTextFieldとボタンを配置しました。
続いて、つぎの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://dev.classmethod.jp/wp-content/uploads/2016/02/006.gif"><img class="alignnone size-full wp-image-181517" src="https://dev.classmethod.jp/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; }
実際に動作している様子は、次のとおりです。
7 最後に
今回は、UIWebViewにおけるナビゲーション関連とコンテンツとのJavaScriptによる連携について、一通りまとめてみました。 まだ、この他にも細かいプロパティは幾つかありますが、ここまでで、大方の作業は可能になるのでは無いでしょうか。