I'm working on a Xamarin mobile app, where the iOS version uses a WKWebView for its UI. Our user input handling works by passing JSON-serialized messages between the app and frame within the webview. To send a message from app to the frame, we use EvaluateJavascript to execute a function that listeners in the frame can respond to. To send a message from the frame to the app, we setup a message port within the frame and use the postMessage API--messages sent this way are received by the WKWebView's didReceiveScriptMessage handler.
Here's the bug. One page of our app includes a file input element, that is:
<input type="file"/>
This should open a file picker menu or dialog for the user to select a file from their device. We've found that this works as expected in most cases--desktop Safari/Chrome, mobile Safari/Chrome, and the Android version of the Xamarin app--but not in the iOS app. What seems to happen is that, in the iOS app, the file input works until we initiate a message loop from within the webview. After that, any file input elements stop responding to clicks. Refreshing or navigating to another page doesn't resolve the issue either--you seemingly have to reset the app at this point.
Abbreviated snippets follow.
Setup the message port (C#):
// add a script message handler when initializing the webview
var config = new WKWebViewConfiguration();
config.UserContentController.AddScriptMessageHandler(new HybridScriptMessageHandler(logger), "AppMessageChannel");
var wkWebView = new WKWebView(UIScreen.MainScreen.Bounds, config);
public void EvaluateJavascript(string script)
{
MainThread.BeginInvokeOnMainThread(() =>
{
Exception handlerEx = null;
base.EvaluateJavaScript(script, (res, err) =>
{
if (err != null)
{
handlerEx = new Exception(err.Domain);
}
});
});
}
// evaluate javascript to configure the message port within the webview frame
var script = @"
(function () {
const loadedHandler = function () {
const event = new CustomEvent('MessagePortReady', { detail: window.webkit.messageHandlers['AppMessageChannel'] });
window.dispatchEvent(event);
};
loadedHandler();
window.addEventListener('DOMContentLoaded', loadedHandler, { once: true });
})();";
EvaluateJavascript(script);
In the frame, add listeners and functions that use the message port (js):
window.addEventListener("MessagePortReady", function (e) {
// set designated message port
MESSAGE_PORT = e.detail;
window.addEventListener("WebViewMessage", function (e) {
const message = JSON.parse(e.data);
// handle the message
});
});
function postMessageToApp(message) {
const messageJson = JSON.stringify(message);
MESSAGE_PORT.postMessage(messageJson);
}
When the postMessageToApp function is called from within the frame, it's received within the app like so (C#):
public override void DidReceiveScriptMessage(WKUserContentController userContentController, WKScriptMessage message)
{
Task.Run(() =>
{
// dispatch an event to the pertinent UI handler
OnMessage?.Invoke(this, new MessageEventArgs((NSString)msg.Body));
});
}
Once we've handled the message (this could involve retrieving data from device storage, making an HTTP request etc.), we use EvaluateJavascript again to post a response back to the frame (C#):
public void PostMessageToUI(string data)
{
// assume that data is some JSON-serialized response object
var script = $@"
(function () {
const msgEvent = new MessageEvent('WebViewMessage', { 'data': '{data}' });
window.dispatchEvent(msgEvent);
})();";
EvaluateJavascript(script);
}
I've tried to make these excerpts concise, but can elaborate if needed. Thank you!
