JavaScript
Android
C#
iOS
Xamarin

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


やりたい事

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にラップしたよーというお話でした。