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);
}
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 の 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 の 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 側と共通化できないので、AddUserScript
で window.webkit.〜
を MyCalc.heavyAdd
にマップしている。
ポイント3。JavaScript からの呼び出しに反応するのは IWKScriptMessageHandler
インターフェース。
端的に言うと、Android と異なる iOS の JavaScript→ネイティブ呼び出しを、AddUserScript で同じAPIにラップしたよーというお話でした。