iOS WKWebView: PostMessage/EvaluateJavascript breaks file input

174 Views Asked by At

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"/>

iOS file input

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!

0

There are 0 best solutions below