ElixirDesktop ネイティブ機能を使用してブラウザを開く(iOS)

# はじめに
この記事は Elixirアドベントカレンダーのシリーズ４の23日目の記事です

今回はWebViewからネイティブ側の関数を実行する方法を解説します
例としてiOSのブラウザを起動して指定したURLを開きます

# 処理の流れ
1. LiveView: phx-hook をセット
2. LiveView: push_eventで hookのthis.handleEventの関数を実行
3. JS Hook: window.webkit.messageHandlers.xx.postMessage(data)を実行
    - xxはiOS側で設定したhandler名 
4. iOS: userContentControllerでhandler名で処理を分ける
5. iOS: ネイティブ機能を実行（URLをSafariで開く)


# iOS側の設定

init() の super.initのあとに
`configuration.userContentController.add(self, name: イベント名)`
でハンドラーを登録します

今回は `openSafari` としています

```diff_swift:native/ios/todoapp/WebView.swift:L40
final class WebView: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
    var webview: WKWebView
    var finish: (() -> ())?
    
    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()

+       configuration.userContentController.add(self, name: "openSafari")

        ...
    }    
}
```

handlerをセットしたら、LiveViewから実行されたときのハンドリングを
`func userContentController`に追加します。
すでにswitch文にerrorがあるので、先程追加した `openSafari`のケースを追加します

なかの処理としては渡されたURLをブラウザで開くという内容です

```diff_swift:native/ios/todoapp/WebView.swift:L89
final class WebView: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
    var webview: WKWebView
    var finish: (() -> ())?

    ...
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        switch message.name {
+       case "openSafari":
+           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`の`openSafari`に対してpostMessageを実行します

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

export default Hooks;
```

Hooksの設定が終わったらLiveView側で呼ぶように実装します

idを付けたタグに phx-hookでHooksのモジュールを読み込みます
phx-clickでLiveViewのイベントを発火させます

```diff_elixir:lib/spotter_web/live/route_live/index.html.heex
<.gheader title="Routes">
  <:actions>
    <.link patch={~p"/routes/new"}>
        <.icon name="hero-plus-solid" class="h-8 w-8 mr-4" />
    </.link>
  </:actions>
</.gheader>

+ <.button id="open_safari" phx-hook="OpenSafari" phx-click="open">
+  open MapLibre
+ </.button>
```

handle_eventで待ち受けし、発火したら push_eventでHooksの関数を実行します

```lib/spotter_web/live/route_live/index.ex
  def handle_event("open", _params, socket) do
    {:noreply, push_event(socket, "open_safari", %{url: "https://maplibre.org/"})}
  end
```

# 動作確認
ブラウザが起動して指定したURLが開かれるのを確認できました

![cbe335300f58e9f9ab0186bd49d7c1f6.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/9af21858-8b55-e28d-866b-4a674441a41f.gif)

# 最後に
本記事ではiOSのネイティブ機能をLiveView側から実行する方法を解説しました
実装の解説は本記事で終了になります

最後はElixirDesktopのアドカレまとめになります

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