Search code examples
javascriptc#iosxamarinwebview

Xamarin iOS HybridWebView does not execute JavaScript files for iOS 15> iPads


I am developing a cross platform app that has a HybridWebView and displays a local html file. I have a wwwroot folder that contains the html file, css files, js files and all other resources. I have built the complete folder as BundleResource. I also start a local web server with EmbedIO. When I launch the app on iPads(iOS 15>), it does not execute the JavaScript files. On iPhones(iOS 15>) the app works fine. Also on iPads with iOS 12 the app works. Also, the app works on Safari no matter what device is used.

I have already added in info.plist NSAppTransportSecurity with NSAllowsArbitraryLoads = true.Also, I have developed a Swift app with a WebView and tried to use the local web server of the Xamarin.iOS app to present the app there. But again JavaScript is not executed (I also set the preferences regarding JavaScript).

My problem:

I don't understand why the application works on Safari, iPhones and old iPads, but not on new iPads. I suspect that you have to enable JavaScript, but can't find a corresponding solution.

To mention:

I load only one js file in the index.html. This js file in turn loads other js files (This works on all devices except the new iPads, as mentioned above).

Below I have added the HybridWebView.cs, the HybridWebViewRenderer.cs, the MainPage.xaml.cs and MainPage.xaml.

MainPage.xaml:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:d="http://xamarin.com/schemas/2014/forms/design"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    xmlns:extensions="clr-namespace:Viewer.Extensions;assembly=Viewer"
    x:Class="Viewer.MainPage">

    <StackLayout>
        <extensions:HybridWebView x:Name="HybridWebView" Uri="{Binding WebViewSource}" VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand" />
    </StackLayout>

</ContentPage>

MainPage.xaml.cs:

namespace Viewer
{
    [DesignTimeVisible(false)]
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class MainPage : ContentPage
    {
        private readonly LocalWebServer _server = new LocalWebServer();

        private string _webViewSource;
        public string WebViewSource
        {
            get => _webViewSource;
            set
            {
                _webViewSource = value;
                OnPropertyChanged(nameof(WebViewSource));
            }
        }

        public MainPage()
        {
            InitializeComponent();
            BindingContext = this;

            NavigationPage.SetHasNavigationBar(this, false);
            HybridWebView.RegisterAction(data =>
            {
                DisplayAlert("Alert", "Hello " + data, "OK");
            });
            HybridWebView.RegisterQRAction(() =>
            {
                try
                {
                    ZXingScannerPage scanPage = new ZXingScannerPage();
                    scanPage.OnScanResult += (result) =>
                    {
                        scanPage.IsScanning = false;

                        Device.BeginInvokeOnMainThread(async () =>
                        {
                            await Navigation.PopAsync();

                            var barcode = result.Text.ParseBarcode();
                            switch (barcode.BarcodeType)
                            {
                                case BarcodeType.Hotspot:
                                    {
                                        await HybridWebView.EvaluateJavaScriptAsync(
                                            $"javascript:doLoadHS('{barcode.Datamodule}.html', '{barcode.Hotspot.figureId}', '{barcode.Hotspot.hotspotId}');");
                                        break;
                                    }
                                case BarcodeType.Datamodule:
                                default:
                                    {
                                        await HybridWebView.EvaluateJavaScriptAsync(
                                            $"javascript:doLoad('{barcode.Datamodule}.html');");
                                        break;
                                    }
                            }
                        });
                    };

                    Device.BeginInvokeOnMainThread(() =>
                    {
                        Navigation.PushAsync(scanPage);
                    });
                }
                catch (Exception ex)
                {
                    DisplayAlert("QR", $"Error while reading qr code: {ex.Message}", "OK");
                }
            });
            HybridWebView.RegisterProjectSelectionAction(() =>
            {
                _server.Dispose();
                Navigation.PopToRootAsync();
            });

            var docpath = Helper.PathAddBackslash(Path.Combine(DependencyService.Get<IApplicationConfigurationService>().DocumentationRootPath, Init.NAME_DIR_WWWROOT));        

            _server.StartWebServer(docpath, false, false);

            WebViewSource = $"{LocalWebServer.Url}/index.html";
            
            NavigationPage.SetHasBackButton(this, false);
        }
        protected override bool OnBackButtonPressed()
        {
            return true;
        }
    }
}

HybridWebView.cs:

using System;
using Xamarin.Forms;

namespace Viewer.Extensions
{
    public class HybridWebView : WebView
    {
        Action<string> action;
        Action qrAction;
        Action projectSelectionAction;

        public static readonly BindableProperty UriProperty = BindableProperty.Create(
            propertyName: "Uri",
            returnType: typeof(string),
            declaringType: typeof(HybridWebView),
            defaultValue: default(string));

        public string Uri
        {
            get { return (string)GetValue(UriProperty); }
            set { SetValue(UriProperty, value); }
        }

        public void RegisterAction(Action<string> callback)
        {
            action = callback;
        }

        public void RegisterQRAction(Action callback)
        {
            qrAction = callback;
        }
        public void RegisterProjectSelectionAction(Action callback)
        {
            projectSelectionAction = callback;
        }

        public void Cleanup()
        {
            action = null;
            qrAction = null;
            projectSelectionAction = null;
        }

        public void InvokeAction(string data)
        {
            if (action == null || data == null)
            {
                return;
            }
            action.Invoke(data);
        }

        public void InvokeQRAction()
        {
            qrAction?.Invoke();
        }

        public void InvokeProjectSelectionAction()
        {
            projectSelectionAction?.Invoke();
        }
    }
}

HybridWebViewRenderer.cs:

[assembly: ExportRenderer(typeof(HybridWebView), typeof(HybridWebViewRenderer))]
namespace Viewer.iOS.Views
{
    public class HybridWebViewRenderer : WkWebViewRenderer, IWKScriptMessageHandler
    {
        const string JavaScriptFunction = "function invokeCSharpAction(data){window.webkit.messageHandlers.invokeAction.postMessage(data);}" +
                                            "function invokeCSharpQRAction(data){window.webkit.messageHandlers.invokeQRAction.postMessage(data);}";

        WKUserContentController userController;

        public HybridWebViewRenderer() : this(new WKWebViewConfiguration())
        {
        }

        public HybridWebViewRenderer(WKWebViewConfiguration config) : base(config)
        {
            userController = config.UserContentController;
            var script = new WKUserScript(new NSString(JavaScriptFunction), WKUserScriptInjectionTime.AtDocumentEnd, false);
            userController.AddUserScript(script);
            userController.AddScriptMessageHandler(this, "invokeAction");
            userController.AddScriptMessageHandler(this, "invokeQRAction");
        }

        protected override void OnElementChanged(VisualElementChangedEventArgs e)
        {
            base.OnElementChanged(e);

            if (e.OldElement != null)
            {
                userController.RemoveAllUserScripts();
                userController.RemoveScriptMessageHandler("invokeAction");
                userController.RemoveScriptMessageHandler("invokeQRAction");
                HybridWebView hybridWebView = e.OldElement as HybridWebView;
                hybridWebView.Cleanup();
            }

            if (e.NewElement != null)
            {
                LoadRequest(new NSUrlRequest(new NSUrl(((HybridWebView)Element).Uri)));
            }

        }

        public void DidReceiveScriptMessage(WKUserContentController userContentController, WKScriptMessage message)
        {
            var eventArgs = message.Body.ToString().ParseEventArgs();
            switch (eventArgs.name)
            {
                case "invokeAction":
                    ((HybridWebView)Element).InvokeAction(eventArgs.payload);
                    break;
                case "invokeQRAction":
                    ((HybridWebView)Element).InvokeQRAction();
                    break;
            }
        }
    }
}

Solution

  • The problem here is probably with your Javascript.

    Mobile and older devices often suppress the console errors of Javascript and instead of an error page or an exception, further Javascript is not executed and it seems as if it is not executed at all.

    Especially with complex Javascript with many usings and references it is enough if there is a problem with one reference, so you don't see anything.

    Here it can help to simplify the Javascript and to rebuild it bit by bit to localize the error.