前回書いた自分専用Todoアプリにもう1つ機能を追加しました。今週のTODO機能です、この機能ではiOSのWKWebView上で動作するJavaScriptとiOSのSwiftコードでデータをやりとりしています。
今週のTODO機能
今週のTODO機能は、今週(近々)に実行しなと行けないタスクの一覧です。また通常のTODOタスクに登録するほどでもない小さなタスクも書かれています。
この一覧には、終了したかどうかのチェックボックスが付いています、本来のTodoアプリのタスク管理と二重管理になってしまいますが・・・
何度か作り直している私のTodoアプリでは、チェックリストとRedmineのようなタスク管理で行ったり来たりしていますが、今回はまたチェックリストが復活しました、ただしメインはRedmineのタスク管理でこの機能は補助的なものです(将来かわるかもしれませんが😅)。
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を組み合わせて作るのも良いかと思います。