はじめに
本記事はElixirDesktopのiOSアプリからネイティブ機能を実行できたので備忘録としてやり方を残しておきます
流れ
- LiveView: phx-hook をセット
- LiveView: push_eventで hookのthis.handleEventの関数を実行
- JS Hook: window.webkit.messageHandlers.xx.postMessage(data)を実行
- xxはiOS側で設定したhandler名
- iOS: userContentControllerでhandler名で処理を分ける
- iOS: ネイティブ機能を実行(URLをSafariで開く)
iOS側の設定
init() の super.initのあとに
configuration.userContentController.add(self, name: handlerName)
でhandler名を設定します
今回は nativeBridge
としています
final class WebView: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
var webview: WKWebView
var finish: (() -> ())?
let handlerName = "nativeBridge"
override init() {
let preferences = WKPreferences()
let page = WKWebpagePreferences()
page.allowsContentJavaScript = true
let configuration = WKWebViewConfiguration()
configuration.limitsNavigationsToAppBoundDomains = true
configuration.preferences = preferences
configuration.defaultWebpagePreferences = page
webview = WKWebView(frame: CGRect.zero, configuration: configuration)
webview.allowsBackForwardNavigationGestures = true
webview.scrollView.isScrollEnabled = true
super.init()
...
# super.initのあとに追加
configuration.userContentController.add(self, name: handlerName)
...
}
}
handlerNameをセットしたら、LiveViewから実行されたときのハンドリングを
func userContentController
に追加します。
すでにswitch文にerrorがあるので、先程追加した nativeBridgeのケースを上辺りに追加します
なかの処理としては渡されたURLをブラウザで開くという内容です
final class WebView: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
var webview: WKWebView
var finish: (() -> ())?
let handlerName = "nativeBridge"
...
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
switch message.name {
// 追加ここから
case "nativeBridge":
print(message.body)
let url = URL(string:message.body as! String)
if( UIApplication.shared.canOpenURL(url!) ) {
UIApplication.shared.open(url!)
}
// 追加ここまで
case "error":
// You should actually handle the error :)
let error = (message.body as? [String: Any])?["message"] as? String ?? "unknown"
assertionFailure("JavaScript error: \(error)")
default:
assertionFailure("Received invalid message: \(message.name)")
}
}
}
LiveView側の設定
Hooksファイルを作成します
mount時に handleEvent open_safariで待ち受けます
ネイティブ側を呼ぶときは先程設定したhandlerName
のnativeBridge
に対してpostMessageを実行します
window.webkit.messageHandlers.nativeBridge.postMessage(url);
Hooks.NativeBridge = {
mounted() {
this.handleEvent("open_safari", ({ url }) => {
window.webkit.messageHandlers.nativeBridge.postMessage(url);
});
},
};
export default Hooks;
Hooksファイルを作成したらapp.jsで読み込みます
import Hooks from "./hooks"; //読み込み
let liveSocket = new LiveSocket("/live", Socket, {
hooks: Hooks, //追加
params: { _csrf_token: csrfToken },
});
Hooksの設定が終わったらLiveView側で呼ぶように実装します
idを付けたタグに phx-hookでHooksのモジュールを読み込みます
phx-clickでLiveViewのイベントを発火させます
<div id="native_test" phx-hook="NativeBridge" >
<a phx-click={JS.push("open_safari", value: %{url: "https://www.phoenixframework.org/"}>
open safari
</a>
</div>
handle_eventで待ち受けし、発火したら push_eventでHooksの関数を実行します
def handle_event("open_safari", %{"url" => url}, socket) do
{:noreply, push_event(socket, "open_safari", %{url: url})
end
demo
別のアプリで試したやつですが、こんな感じに動きます
最後に
ベースとなる枠組みはすでにあったらので、実行する関数毎にハンドラーを設定すれば、簡単にネイティブ機能を実行できそうでした
あとはセンサーデータ等を使う双方向のやり方をマスターすればバッチリですね