File: iOS\IOSWebViewManager.cs
Web Access
Project: src\src\BlazorWebView\src\Maui\Microsoft.AspNetCore.Components.WebView.Maui.csproj (Microsoft.AspNetCore.Components.WebView.Maui)
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Encodings.Web;
using Foundation;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using UIKit;
using WebKit;
 
namespace Microsoft.AspNetCore.Components.WebView.Maui
{
	/// <summary>
	/// An implementation of <see cref="WebViewManager"/> that uses the <see cref="WKWebView"/> browser control
	/// to render web content.
	/// </summary>
	internal class IOSWebViewManager : WebViewManager
	{
		private readonly BlazorWebViewHandler _blazorMauiWebViewHandler;
		private readonly ILogger _logger;
		private readonly WKWebView _webview;
		private readonly string _contentRootRelativeToAppRoot;
 
		/// <summary>
		/// Initializes a new instance of <see cref="IOSWebViewManager"/>
		/// </summary>
		/// <param name="blazorMauiWebViewHandler">The <see cref="BlazorWebViewHandler"/>.</param>
		/// <param name="webview">The <see cref="WKWebView"/> to render web content in.</param>
		/// <param name="provider">The <see cref="IServiceProvider"/> for the application.</param>
		/// <param name="dispatcher">A <see cref="Dispatcher"/> instance instance that can marshal calls to the required thread or sync context.</param>
		/// <param name="fileProvider">Provides static content to the webview.</param>
		/// <param name="jsComponents">Describes configuration for adding, removing, and updating root components from JavaScript code.</param>
		/// <param name="contentRootRelativeToAppRoot">Path to the directory containing application content files.</param>
		/// <param name="hostPageRelativePath">Path to the host page within the fileProvider.</param>
		/// <param name="logger">Logger to send log messages to.</param>
 
		public IOSWebViewManager(BlazorWebViewHandler blazorMauiWebViewHandler, WKWebView webview, IServiceProvider provider, Dispatcher dispatcher, IFileProvider fileProvider, JSComponentConfigurationStore jsComponents, string contentRootRelativeToAppRoot, string hostPageRelativePath, ILogger logger)
			: base(provider, dispatcher, BlazorWebViewHandler.AppOriginUri, fileProvider, jsComponents, hostPageRelativePath)
		{
			ArgumentNullException.ThrowIfNull(blazorMauiWebViewHandler);
			ArgumentNullException.ThrowIfNull(webview);
 
			if (provider.GetService<MauiBlazorMarkerService>() is null)
			{
				throw new InvalidOperationException(
					"Unable to find the required services. " +
					$"Please add all the required services by calling '{nameof(IServiceCollection)}.{nameof(BlazorWebViewServiceCollectionExtensions.AddMauiBlazorWebView)}' in the application startup code.");
			}
 
			_logger = logger;
			_blazorMauiWebViewHandler = blazorMauiWebViewHandler;
			_webview = webview;
			_contentRootRelativeToAppRoot = contentRootRelativeToAppRoot;
 
			InitializeWebView();
		}
 
		/// <inheritdoc />
		protected override void NavigateCore(Uri absoluteUri)
		{
			_logger.NavigatingToUri(absoluteUri);
			using var nsUrl = new NSUrl(absoluteUri.ToString());
			using var request = new NSUrlRequest(nsUrl);
			_webview.LoadRequest(request);
		}
 
		internal bool TryGetResponseContentInternal(string uri, bool allowFallbackOnHostPage, out int statusCode, out string statusMessage, out Stream content, out IDictionary<string, string> headers)
		{
			var defaultResult = TryGetResponseContent(uri, allowFallbackOnHostPage, out statusCode, out statusMessage, out content, out headers);
			var hotReloadedResult = StaticContentHotReloadManager.TryReplaceResponseContent(_contentRootRelativeToAppRoot, uri, ref statusCode, ref content, headers);
			return defaultResult || hotReloadedResult;
		}
 
		/// <inheritdoc />
		protected override void SendMessage(string message)
		{
			var messageJSStringLiteral = JavaScriptEncoder.Default.Encode(message);
			_webview.EvaluateJavaScript(
				javascript: $"__dispatchMessageCallback(\"{messageJSStringLiteral}\")",
				completionHandler: (NSObject result, NSError error) => { });
		}
 
		internal void MessageReceivedInternal(Uri uri, string message)
		{
			MessageReceived(uri, message);
		}
 
		private void InitializeWebView()
		{
			_webview.NavigationDelegate = new WebViewNavigationDelegate(_blazorMauiWebViewHandler);
			_webview.UIDelegate = new WebViewUIDelegate(_blazorMauiWebViewHandler);
		}
 
		internal sealed class WebViewUIDelegate : WKUIDelegate
		{
			private static readonly string LocalOK = NSBundle.FromIdentifier("com.apple.UIKit").GetLocalizedString("OK");
			private static readonly string LocalCancel = NSBundle.FromIdentifier("com.apple.UIKit").GetLocalizedString("Cancel");
			private readonly BlazorWebViewHandler _webView;
 
			public WebViewUIDelegate(BlazorWebViewHandler webView)
			{
				_webView = webView ?? throw new ArgumentNullException(nameof(webView));
			}
 
 
			public override void RunJavaScriptAlertPanel(WKWebView webView, string message, WKFrameInfo frame, Action completionHandler)
			{
				PresentAlertController(
					webView,
					message,
					okAction: _ => completionHandler()
				);
			}
 
			public override void RunJavaScriptConfirmPanel(WKWebView webView, string message, WKFrameInfo frame, Action<bool> completionHandler)
			{
				PresentAlertController(
					webView,
					message,
					okAction: _ => completionHandler(true),
					cancelAction: _ => completionHandler(false)
				);
			}
 
			public override void RunJavaScriptTextInputPanel(
				WKWebView webView, string prompt, string? defaultText, WKFrameInfo frame, Action<string> completionHandler)
			{
				PresentAlertController(
					webView,
					prompt,
					defaultText: defaultText,
					okAction: x => completionHandler(x.TextFields[0].Text!),
					cancelAction: _ => completionHandler(null!)
				);
			}
 
			private static string GetJsAlertTitle(WKWebView webView)
			{
				// Emulate the behavior of UIWebView dialogs.
				// The scheme and host are used unless local html content is what the webview is displaying,
				// in which case the bundle file name is used.
				if (webView.Url != null && webView.Url.AbsoluteString != $"file://{NSBundle.MainBundle.BundlePath}/")
					return $"{webView.Url.Scheme}://{webView.Url.Host}";
 
				return new NSString(NSBundle.MainBundle.BundlePath).LastPathComponent;
			}
 
			private static UIAlertAction AddOkAction(UIAlertController controller, Action handler)
			{
				var action = UIAlertAction.Create(LocalOK, UIAlertActionStyle.Default, (_) => handler());
				controller.AddAction(action);
				controller.PreferredAction = action;
				return action;
			}
 
			private static UIAlertAction AddCancelAction(UIAlertController controller, Action handler)
			{
				var action = UIAlertAction.Create(LocalCancel, UIAlertActionStyle.Cancel, (_) => handler());
				controller.AddAction(action);
				return action;
			}
 
			private static void PresentAlertController(
				WKWebView webView,
				string message,
				string? defaultText = null,
				Action<UIAlertController>? okAction = null,
				Action<UIAlertController>? cancelAction = null)
			{
				var controller = UIAlertController.Create(GetJsAlertTitle(webView), message, UIAlertControllerStyle.Alert);
 
				if (defaultText != null)
					controller.AddTextField((textField) => textField.Text = defaultText);
 
				if (okAction != null)
					AddOkAction(controller, () => okAction(controller));
 
				if (cancelAction != null)
					AddCancelAction(controller, () => cancelAction(controller));
 
#pragma warning disable CA1416, CA1422 // TODO:  'UIApplication.Windows' is unsupported on: 'ios' 15.0 and later
				GetTopViewController(UIApplication.SharedApplication.Windows.FirstOrDefault(m => m.IsKeyWindow)?.RootViewController)?
					.PresentViewController(controller, true, null);
#pragma warning restore CA1416, CA1422
			}
 
			private static UIViewController? GetTopViewController(UIViewController? viewController)
			{
				if (viewController is UINavigationController navigationController)
					return GetTopViewController(navigationController.VisibleViewController);
 
				if (viewController is UITabBarController tabBarController)
					return GetTopViewController(tabBarController.SelectedViewController!);
 
				if (viewController?.PresentedViewController != null)
					return GetTopViewController(viewController.PresentedViewController);
 
				return viewController;
			}
		}
 
		internal class WebViewNavigationDelegate : WKNavigationDelegate
		{
			private readonly BlazorWebViewHandler _webView;
 
			private WKNavigation? _currentNavigation;
			private Uri? _currentUri;
 
			public WebViewNavigationDelegate(BlazorWebViewHandler webView)
			{
				_webView = webView ?? throw new ArgumentNullException(nameof(webView));
			}
 
			public override void DidStartProvisionalNavigation(WKWebView webView, WKNavigation navigation)
			{
				_currentNavigation = navigation;
			}
 
			public override void DecidePolicy(WKWebView webView, WKNavigationAction navigationAction, Action<WKNavigationActionPolicy> decisionHandler)
			{
				var requestUrl = navigationAction.Request.Url;
				var uri = new Uri(requestUrl.ToString());
 
				UrlLoadingStrategy strategy;
 
				// TargetFrame is null for navigation to a new window (`_blank`)
				if (navigationAction.TargetFrame is null)
				{
					// Open in a new browser window regardless of UrlLoadingStrategy
					strategy = UrlLoadingStrategy.OpenExternally;
				}
				else
				{
					// Invoke the UrlLoading event to allow overriding the default link handling behavior
					var callbackArgs = UrlLoadingEventArgs.CreateWithDefaultLoadingStrategy(uri, BlazorWebViewHandler.AppOriginUri);
					_webView.UrlLoading(callbackArgs);
					_webView.Logger.NavigationEvent(uri, callbackArgs.UrlLoadingStrategy);
 
					strategy = callbackArgs.UrlLoadingStrategy;
				}
 
				if (strategy == UrlLoadingStrategy.OpenExternally)
				{
					_webView.Logger.LaunchExternalBrowser(uri);
 
#pragma warning disable CA1416, CA1422 // TODO: OpenUrl(...) has [UnsupportedOSPlatform("ios10.0")]
					UIApplication.SharedApplication.OpenUrl(requestUrl);
#pragma warning restore CA1416, CA1422
				}
 
				if (strategy != UrlLoadingStrategy.OpenInWebView)
				{
					// Cancel any further navigation as we've either opened the link in the external browser
					// or canceled the underlying navigation action.
					decisionHandler(WKNavigationActionPolicy.Cancel);
					return;
				}
 
				if (navigationAction.TargetFrame!.MainFrame)
				{
					_currentUri = requestUrl;
				}
 
				decisionHandler(WKNavigationActionPolicy.Allow);
			}
 
			public override void DidReceiveServerRedirectForProvisionalNavigation(WKWebView webView, WKNavigation navigation)
			{
				// We need to intercept the redirects to the app scheme because Safari will block them.
				// We will handle these redirects through the Navigation Manager.
				if (_currentUri?.Host == BlazorWebView.AppHostAddress)
				{
					var uri = _currentUri;
					_currentUri = null;
					_currentNavigation = null;
					if (uri is not null)
					{
						var request = new NSUrlRequest(new NSUrl(uri.AbsoluteUri));
						webView.LoadRequest(request);
					}
				}
			}
 
			public override void DidFailNavigation(WKWebView webView, WKNavigation navigation, NSError error)
			{
				_currentUri = null;
				_currentNavigation = null;
			}
 
			public override void DidFailProvisionalNavigation(WKWebView webView, WKNavigation navigation, NSError error)
			{
				_currentUri = null;
				_currentNavigation = null;
			}
 
			public override void DidCommitNavigation(WKWebView webView, WKNavigation navigation)
			{
				if (_currentUri != null && _currentNavigation == navigation)
				{
					// TODO: Determine whether this is needed
					//_webView.HandleNavigationStarting(_currentUri);
				}
			}
 
			public override void DidFinishNavigation(WKWebView webView, WKNavigation navigation)
			{
				if (_currentUri != null && _currentNavigation == navigation)
				{
					// TODO: Determine whether this is needed
					//_webView.HandleNavigationFinished(_currentUri);
					_currentUri = null;
					_currentNavigation = null;
				}
			}
		}
	}
}