EY-Office ブログ

iOSのWKWebView上で動作するJavaScriptとのやりとり

前回書いた自分専用Todoアプリにもう1つ機能を追加しました。今週のTODO機能です、この機能ではiOSのWKWebView上で動作するJavaScriptとiOSのSwiftコードでデータをやりとりしています。

WebView and JavaScript

今週のTODO機能

今週のTODO機能は、今週(近々)に実行しなと行けないタスクの一覧です。また通常のTODOタスクに登録するほどでもない小さなタスクも書かれています。
この一覧には、終了したかどうかのチェックボックスが付いています、本来のTodoアプリのタスク管理と二重管理になってしまいますが・・・

何度か作り直している私のTodoアプリでは、チェックリストとRedmineのようなタスク管理で行ったり来たりしていますが、今回はまたチェックリストが復活しました、ただしメインはRedmineのタスク管理でこの機能は補助的なものです(将来かわるかもしれませんが😅)。

TodoTask App

WKWebView上で動作するJavaScriptとiOS(Swift)のやりとり

上画像の画像でわかるように(?)タスクの詳細はMarkdownで書けるようになっています。Markdownを表示できるSwiftのライブラリーもありますが、このアプリの元祖を作りだした頃は良いライブラリーが発見出来なかったのでWKWebView上でJavaScript製のMarkedライブラリーを動かすることにしました。

iOS(Swift)からJavaScriptをコントロール

WKWebViewにはevaluateJavaScript(javaScriptString, completionHandler) というSwiftのメソッドがあり、WKWebView上でJavaScriptのコードを実行できます。当然WKWebView上のHTMLに定義されているJavaScript関数を引数を渡し実行できます。関数の実行結果もcompletionHandlerで受け取れます。

JavaScriptからiOS(Swift)をコントロール

今回のアプリではチェックボックスをクリックしたさいに、結果をサーバー等に反映するためiOSの関数を呼び出したくなります。そのためにWKWebViewにはWKUserContentControllerというクラスが用意されています。これを使うとiOS側で登録されたメッセージハンドラーをJavaScriptから呼び出せます。

準備

下のコードのように画面が作成された時点等にWKWebViewの.configuration.userContentController.addメソッドでメッセージハンドラー(の名前)を定義します。

override func viewDidLoad() {
  super.viewDidLoad()

    ・・・

  discriptionWeb.configuration.userContentController.add(self, name: "changeCheckbox")
}
JavaScript側呼び出しコード
  • ① 全チェックボックス要素のchangeイベントに対してハンドラーを定義しています
  • ② ページ上の何番目のチェックボックスが変更したのかを探しposition変数に代入します
  • ③ ②でもとめた何番目を10倍し、チェックボックスがチェックされているなら1を加え変更されたチェックボックスの場所+状態を、整数値に組み立ています
  • ④ ③で計算したデータを引数にし、メッセージハンドラー .messageHandlers.changeCheckbox.postMessage()を呼び出し、iOSにデータを渡します
document.querySelectorAll("input[type=checkbox]").forEach(e => {
  e.addEventListener("change", event => {     // ← ①
    const postion = Array.from(document.querySelectorAll("input[type=checkbox]")).
        findIndex((checkbox) => checkbox == event.target)    // ← ②
    const data = postion * 10 + Number(event.target.checked) // ← ③
    webkit.messageHandlers.changeCheckbox.postMessage(data)  // ← ④
  });
});
Swift呼び出されるコード
  • ① UIViewController内にメッセージハンドラーのuserContentControllerメソッドを定義します
  • ② JavaScriptから渡されたデータを取得します
  • ③ チェックボックスの何番目、チェック済み状態を算出
  • ④ Markdownのソースではチェックボックスは - [ ]、チェック済みは - [x]と書かれているのでそれを正規表現を使い、ソース内の全チェックボックス(の場所)を取得
  • ⑤ 変更されたチェックボックスに対応するMarkdownソースをチェック済み・未チェックを書き換える
  • ⑥ 変更されたMarkdownソースをテキストViewに設定
  • ⑦ 改めてマークダウンを再表示
func userContentController(_ userContentController: WKUserContentController,
                           didReceive message: WKScriptMessage) { // ← ①
  switch message.name {
  case "changeCheckbox":                // ←  ②
    let param = message.body as! Int
    let postion = param / 10            // ←  ③
    let checked = (param % 10) == 1     // ←  ③
    var source = discriptionText.text ?? ""
    let regexp = try! NSRegularExpression(pattern: #"- \[.\] "#)
    let matches = regexp.matches(in: source,
            range: NSRange(source.startIndex..., in: source))  // ←  ④
    if matches.count > postion {
        source.replaceSubrange(Range(matches[postion].range, in: source)!,
                with: checked ? "- [x] " : "- [ ] ")   // ←  ⑤
        discriptionText.text = source                  // ←  ⑥
        refreshMarkedown(source)                       // ←  ⑦
        ・・・・

  case "changeCheckbox":         // 他のイベント処理
        ・・・・
    }
  }
}

まとめ

少し面倒ですが、WKWebView上で動作するJavaScriptとデータや処理のやりとりができるので、すでにWeb(HTML+CSS+JavaScript)で動くコードがある場合は、すべてSwiftで書かずにWebを組み合わせて作るのも良いかと思います。

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ