8
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ElixirAdvent Calendar 2024

Day 9

ElixirDesktopで作るスマホアプリ Part 5 ネイティブブリッジを使って特定のURLをデフォルトブラウザで開く

Posted at

はじめに

この記事はElixirアドベントカレンダー2024のシリーズ2、8日目の記事です

WebViewで表示しているWebアプリからネイティブブリッジを通してiOS、Androidのアプリから別のアプリ(ブラウザ)を呼び出す機能を実装していきます

今回の作業ブランチを作成します

git checkout -b feature/naive_bridge

ネイティブブリッジとは

Native BridgeとはWebViewとネイティブアプリとの連携を表す言葉で、Webからネイティブアプリのメソッドを呼び出したり、ネイティブアプリから表示しているWebページのJavascriptのメソッドを呼び出すことができます。
by https://engineering.mercari.com/blog/entry/20210814-6694cc7502/

ElixirDesktopはPhoenixのWebアプリケーションと各OSのアプリでWebViewを起動するネイティブ部分に分かれており

Webアプリケーション側から特定のJavaScript関数を実行し、
ネイティブのWebView側で設定した特定の関数の実行をトリガーとして
外部のブラウザを開く、別のアプリを開く、各種センサーデータの取得、課金機能などが実行できます

Web app -> JS Handler -> OSネイティブ機能

各OSのブリッジ

各OSのブリッジについて解説します

iOS WKScriptMessageHandler

iOSではWKScriptMessageHandlerを使用します

WKScriptMessageHandlerを継承したクラス作成し、
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage)のメソッドを実装します

そして、init関数内でWKWebViewConfigurationuserContentControllerを引数にしてWebViewを作成することで、WebViewで表示しているWebアプリのJavaScriptからwindow.webkit.messageHandlers.[メソッド名].postMessage(ペイロード)で呼び出すことが出来ます

WebView.swift
final class WebView: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
    var webview: WKWebView
    
    override init() {
        ...
        let configuration = WKWebViewConfiguration()        
        configuration.userContentController.add(self, name: [メソッド名])
        webview = WKWebView(frame: CGRect.zero, configuration: configuration)
        ...
    }

    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        switch message.name {
        case "[メソッド名]":
          //  ブラウザを開くなどネイティブ機能を実行する処理を追加
          //  callbackを行いたい場合は evaluateJavaScriptで行う
          //  ペイロード等は message.bodyで取得できる
        default: // 設定していないメソッド名を実行された時の処理
            assertionFailure("Received invalid message: \(message.name)")
        }
    }

}

Android

AndroidではWebAppInterfaceを使用します

MainActivityクラスの外にWebAppInterfaceクラスを作成し、@JavascriptInterfaceアノテーションを付けた関数を作ることで、WebViewで表示しているWebアプリケーションからAndroid.method_nameで呼ぶことが出来ます

MainActivity.kt
class WebAppInterface(private val mContext: Context) {

    @JavascriptInterface
    fun method_name (payload) {
        // ネイティブ処理をここに書く
        // コールバックが欲しい場合は
        // wevbiew.loadUrl("javascript:実行するJSコード")
    }
}

onCreate内でWebAppInterfaceインスタンスを作成し、WebViewにインターフェイスを追加して完了です

MainActivity.kt
class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        val webView: WebView = findViewById(R.id.webview)
        val webInterface = WebAppInterface(this@MainActivity)
        webView.addJavascriptInterface(webAppInterface, "Android")
        ...
    }
}

実装

iOS,Androidで実装方法がわかったので外部のURLをクリックした際にOSのデフォルトブラウザで開くようにします

iOS側

desktop_setupのテンプレートで作成した場合すでに実装済みですので該当箇所のハイライトを行います

super.init()実行後に以下を追加します

configuration.userContentController.add(self, name: [関数名])

今回はopenSafariとなり、別の関数を追加したい場合は同様のコードを下に追加していきます

native/ios/Trarecord/WebView.swift:L18
    override init() {
        // Enable javascript in WKWebView to interact with the web app
        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()
        
+       configuration.userContentController.add(self, name: "openSafari")
        ....
    }

userController関数にopenSafariが呼ばれた時の処理を追加します

canOpenURLで開くことができるURLかチェックを行い、問題なければopenでデフォルトに指定しているブラウザでURLを開きます

native/ios/Trarecord/WebView.swift:L89
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        switch message.name {
+       case "openSafari":
+           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)")
        }
    }

Android

Android,デスクトップアプリの場合は特に手を加えること無く開けるので特にやることはありません

Phoenix

Webアプリケーション側の実装を行います

今回はLiveViewとJavaScript間で相互に関数の実行やデータのやり取りができる JS Hooksを使ってネイティブブリッジを作っていきます

最初にhooksフォルダをassets配下に作ります

mkdir assets/hooks

ネイティブブリッジにopen_safariを追加します。ネイティブブリッジを使うのはiOSだけなので、iOSだけ正常に終了して、他のOSだと例外を握りつぶして何も起こらないようにします。

Android,iOSで分ける場合は、UAを見るかAndroidがundefかどうかで切り分けます

assets/js/hooks/NativeBridge.js
const NativeBridge = {
  mounted() {
    this.handleEvent("open_safari", ({ url }) => {
      try {
        window.webkit.messageHandlers.openSafari.postMessage(url);
      } catch {}
    });
  },
};

export default NativeBridge;

ブリッジが出来たのでindex.jsでまとめます

assets/js/hooks/index.js
import NativeBridge from "./NativeBridge";

export default Hooks = {
  NativeBridge: NativeBridge,
};

JS Hookが出来たのでapp.jsで読み込みます

assets/js/app.js
import "phoenix_html";
// Establish Phoenix Socket and LiveView configuration.
import { Socket } from "phoenix";
import { LiveSocket } from "phoenix_live_view";
import topbar from "../vendor/topbar";
+ import Hooks from "./hooks";

let csrfToken = document
  .querySelector("meta[name='csrf-token']")
  .getAttribute("content");
let liveSocket = new LiveSocket("/live", Socket, {
+ hooks: Hooks,
  longPollFallbackMs: 2500,
  params: { _csrf_token: csrfToken },
});

JS Hooksが出来たので実行する箇所で読み込み、イベントトリガーを追加します

JS Hooksを読み込む箇所はid属性をつける必要があります
phx-click="イベント名"handle_event/3関数で待ち受けているイベントを発火させます
phx-value-[パラメーター名]={パラメーター}handle_event/3関数の第2引数に値を渡せます

lib/trarecord_web/live/onboarding_live/index.ex:L4
  def render(assigns) do
    ~H"""
    <div
      class="flex justify-center w-screen h-screen bg-local bg-contain bg-center bg-no-repeat"
      style={"background-image: url(#{@page_data.image}); background-color: #{@page_data.bg}"}
    >
    ...
-   <p id="freepik" class="absolute bottom-2 right-4 text-sm">
+   <p id="freepik" class="absolute bottom-2 right-4 text-sm" phx-hook="NativeBridge">   
      <a
        href={@page_data.link}
        class="underline text-blue-600"
        target="_blank"
+       phx-click="open_safari"
+       phx-value-url={@page_data.link}
      >
        Image by storyset on Freepik
      </a>
    </p>
    """
  end

push_event/3関数でJS Hookのthis.handleEventで実装しているイベントを発火させます
第3引数は渡す値がなくても必須で%{}(空のマップ)を渡します

lib/trarecord_web/live/onboarding_live/index.ex:L49
  def handle_event("finish", _params, socket) do
    {:noreply, push_navigate(socket, to: ~p"/")}
  end

+ def handle_event("open_safari", %{"url" => url}, socket) do
+   socket
+   |> push_event("open_safari", %{url: url})
+   |> then(&{:noreply, &1})
+ end

動作確認

iOSで問題なくデフォルトブラウザでURLを開くことが出来ました
またAndroid、デスクトップアプリでURLをブラウザで開けることが確認できました

e1fdaa449336cbfced89f2714db3261c.gif

今回はネイティブブリッジのテストはLiveviewの範囲外になるため、テストは書けませんがplaywright-elixirを使えば書けるかもしれません

最後に

今回はネイティブブリッジの解説と実装JS Hookについて解説しました
次は行きたいスポットをグループ分けするフォルダのCRUD画面とリレーションに付いて解説します

本記事は以上になりますありがとうございました

参考サイト

https://developer.apple.com/documentation/webkit/wkscriptmessagehandler
https://qiita.com/the_haigo/items/aed1cc6b57d596dc0dc1
https://smile-jsp.hateblo.jp/entry/android/webview-js-to-native-code
https://engineering.mercari.com/blog/entry/20210814-6694cc7502/
https://qiita.com/atsuto/items/1acb87ce9f0512cf8641
https://github.com/mechanical-orchard/playwright-elixir
https://qiita.com/RyoWakabayashi/items/ca2924b652ec358008d7

8
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?