LoginSignup
16
10

More than 5 years have passed since last update.

Xamarin.Forms の WebView で JavaScript 連携を行う(with iOS/Android共通化)

Last updated at Posted at 2018-12-18

やりたい事

Xamarin.Forms 製アプリの WebView に表示した Webページから、ネイティブ(C#)で時間のかかる処理を行い、結果を JavaScript に通知したい。
JavaScript のコードは Android/iOS で共通にしたい。

具体的には、次のような JavaScript コードの heavyAdd(num) を実行した時に、ネイティブ側で処理を行い、結果を onResult(res) で受信したい。

function addAsync() {  
  MyCalc.onResult = function (res) {
    var label = document.getElementById("result");
    label.innerHTML = 'MyCalc.onResult - ' + res;
  };
  MyCalc.heavyAdd(98);
}

できた!

  • 前提 - Xamarin.Forms 3.4.x が必要

共通

sample.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <script>
    function addAsync() {

      MyCalc.onResult = function (res) {
        var label = document.getElementById("result");
        label.innerHTML = 'MyCalc.onResult - ' + res;
      };

      MyCalc.heavyAdd(98);
    }
  </script>
</head>
<body>
  <h1>WebView−JavaScript連携サンプル</h1>
  <p><button onclick="addAsync();">計算実行</button></p>
  <label id="result"></label>
</body>
</html>

ローカルPC にある sample.html は、 Webサーバー(npm serve とか)を立てて、 ngrok を使って外部公開するのが便利ですね。

MainPage.xaml

<?xml version="1.0" encoding="utf-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
    xmlns:local="clr-namespace:WebViewSample" x:Class="WebViewSample.MainPage">
    <StackLayout Orientation="Vertical">
        <WebView x:Name="webView" 
                 Source="https://xxxx.ngrok.io/sample.html"
                 VerticalOptions="FillAndExpand"/>
    </StackLayout>
</ContentPage>

Android側

Android の CustomWebViewRenderer.cs

using System;
using System.Threading.Tasks;
using Android.Content;
using Java.Interop;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;

[assembly: ExportRenderer(typeof(WebView), typeof(WebViewSample.Droid.CustomWebViewRenderer))]

namespace WebViewSample.Droid
{
    public class CustomWebViewRenderer : Xamarin.Forms.Platform.Android.WebViewRenderer
    {
        public CustomWebViewRenderer(Context context) : base(context) { }

        protected override void OnElementChanged(ElementChangedEventArgs<WebView> e)
        {
            base.OnElementChanged(e);
            Control.AddJavascriptInterface(new JavaScriptHandler(Control), "MyCalc");
        }
    }

    class JavaScriptHandler : Java.Lang.Object
    {
        private readonly Android.Webkit.WebView webView;

        public JavaScriptHandler(Android.Webkit.WebView webView)
        {
            this.webView = webView;
        }

        [Export]
        [Android.Webkit.JavascriptInterface]
        async public void heavyAdd(int num)
        {
            await Task.Delay(1000);
            var result = num * 2;

            // メインスレッドから呼ばないとエラー
            this.webView.Post(() => 
            {
                this.webView.LoadUrl($"javascript:MyCalc.onResult({result});");
            });
        }
    }
}

を参考に、ネイティブのやり方をカスタムレンダラーで。
Android の方はまだ単純で AddJavascriptInterface() の第2引数がクラス名に、JavascriptInterface 属性を付けたメソッドが JavaScript のメソッド名になる。
結果の通知は this.webView.LoadUrl($"javascript:MyCalc.onResult(xx); で。

iOS側

iOS の CustomWebViewRenderer.cs

using System;
using System.Threading.Tasks;
using Foundation;
using WebKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;

[assembly: ExportRenderer(typeof(WebView), typeof(WebViewSample.iOS.CustomWebViewRenderer))]

namespace WebViewSample.iOS
{
    public class CustomWebViewRenderer : Xamarin.Forms.Platform.iOS.WkWebViewRenderer, IWKScriptMessageHandler
    {
        protected override void OnElementChanged(VisualElementChangedEventArgs e)
        {
            base.OnElementChanged(e);

            var webView = this.NativeView as WKWebView;

            // JavaScript から呼び出すハンドラを追加。
            webView.Configuration.UserContentController.AddScriptMessageHandler(this, "MyHeavyAdd");

            // JavaScript 側で MyCalc.heavyAdd(n) が呼ばれた時に window.webkit.messageHandlers.xxx を呼ぶようにする。
            var script =
                "MyCalc = {};" +
                "MyCalc.heavyAdd = function (num) { window.webkit.messageHandlers.MyHeavyAdd.postMessage(num); };";
            webView.Configuration.UserContentController.AddUserScript(new WKUserScript(
                new NSString(script), WKUserScriptInjectionTime.AtDocumentStart, true));
        }

        async void IWKScriptMessageHandler.DidReceiveScriptMessage(WKUserContentController userContentController, WKScriptMessage message)
        {
            if (message.Name == "MyHeavyAdd") 
            {
                // 時間のかかる処理
                await Task.Delay(1000);
                var result = (message.Body as NSNumber).Int32Value * 2;

                // 結果を通知
                var webView = this.NativeView as WKWebView;
                webView.EvaluateJavaScript($"MyCalc.onResult({result});", null);
            }
        }
    }
}

を参考にカスタムレンダラーで実装。

ポイント1。Xamarin.Forms 3.4から? WebView の実装が WKWebView になった模様。それまでは(少なくとも Xamarin.Forms 3.1 では) UIWebView だった。
Xamarin.Forms 3.4 でないと Xamarin.Forms.Platform.iOS.WkWebViewRenderer が存在しないため使えない。

ポイント2。iOS で JavaScript からネイティブの処理を呼ぶには window.webkit.messageHandlers.xxxx.postMessage() を使わなければならないが、これでは Android 側と共通化できないので、AddUserScriptwindow.webkit.〜MyCalc.heavyAdd にマップしている。

ポイント3。JavaScript からの呼び出しに反応するのは IWKScriptMessageHandler インターフェース。

こんな感じ

image.png

端的に言うと、Android と異なる iOS の JavaScript→ネイティブ呼び出しを、AddUserScript で同じAPIにラップしたよーというお話でした。

16
10
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
16
10