|
using System;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.IO;
using System.Threading.Tasks;
using CoreGraphics;
using Foundation;
using Microsoft.Extensions.Logging;
using UIKit;
using WebKit;
namespace Microsoft.Maui.Platform
{
public class MauiWKWebView : WKWebView, IWebViewDelegate, IUIViewLifeCycleEvents
{
[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Used to persist cookies across WebView instances. Not a leak.")]
static WKProcessPool? SharedPool;
string? _pendingUrl;
readonly WeakReference<WebViewHandler> _handler;
public MauiWKWebView(WebViewHandler handler)
: this(RectangleF.Empty, handler)
{
}
public MauiWKWebView(CGRect frame, WebViewHandler handler)
: this(frame, handler, CreateConfiguration())
{
}
public MauiWKWebView(CGRect frame, WebViewHandler handler, WKWebViewConfiguration configuration)
: base(frame, configuration)
{
_ = handler ?? throw new ArgumentNullException(nameof(handler));
_handler = new WeakReference<WebViewHandler>(handler);
BackgroundColor = UIColor.Clear;
AutosizesSubviews = true;
NavigationDelegate = new MauiWebViewNavigationDelegate(handler);
}
public string? CurrentUrl =>
Url?.AbsoluteUrl?.ToString();
public override void MovedToWindow()
{
base.MovedToWindow();
if (!string.IsNullOrWhiteSpace(_pendingUrl))
{
var closure = _pendingUrl;
_pendingUrl = null;
// I realize this looks like the worst hack ever but iOS 11 and cookies are super quirky
// and this is the only way I could figure out how to get iOS 11 to inject a cookie
// the first time a WkWebView is used in your app. This only has to run the first time a WkWebView is used
// anywhere in the application. All subsequents uses of WkWebView won't hit this hack
// Even if it's a WkWebView on a new page.
// read through this thread https://developer.apple.com/forums/thread/99674
// Or Bing "WkWebView and Cookies" to see the myriad of hacks that exist
// Most of them all came down to different variations of synching the cookies before or after the
// WebView is added to the controller. This is the only one I was able to make work
// I think if we could delay adding the WebView to the Controller until after ViewWillAppear fires that might also work
// But we're not really setup for that
// If you'd like to try your hand at cleaning this up then UI Test Issue12134 and Issue3262 are your final bosses
InvokeOnMainThread(async () =>
{
await Task.Delay(500);
if (_handler.TryGetTarget(out var handler))
await handler.FirstLoadUrlAsync(closure);
});
}
_movedToWindow?.Invoke(this, EventArgs.Empty);
}
[Obsolete("Use MauiWebViewNavigationDelegate.DidFinishNavigation instead.")]
public async void DidFinishNavigation(WKWebView webView, WKNavigation navigation)
{
var url = CurrentUrl;
if (url == null || url == $"file://{NSBundle.MainBundle.BundlePath}/")
return;
if (_handler.TryGetTarget(out var handler))
await handler.ProcessNavigatedAsync(url);
}
[Export("webViewWebContentProcessDidTerminate:")]
public void ContentProcessDidTerminate(WKWebView webView)
{
if (_handler.TryGetTarget(out var handler))
handler.VirtualView.ProcessTerminated(new WebProcessTerminatedEventArgs(webView));
}
public void LoadHtml(string? html, string? baseUrl)
{
if (html != null)
LoadHtmlString(html, baseUrl == null ? new NSUrl(NSBundle.MainBundle.BundlePath, true) : new NSUrl(baseUrl, true));
}
async Task LoadUrlAsync(string? url)
{
try
{
var uri = new Uri(url ?? string.Empty);
var safeHostUri = new Uri($"{uri.Scheme}://{uri.Authority}", UriKind.Absolute);
var safeRelativeUri = new Uri($"{uri.PathAndQuery}{uri.Fragment}", UriKind.Relative);
var safeFullUri = new Uri(safeHostUri, safeRelativeUri);
NSUrlRequest request = new NSUrlRequest(new NSUrl(safeFullUri.AbsoluteUri));
if (_handler.TryGetTarget(out var handler))
{
if (handler.HasCookiesToLoad(safeFullUri.AbsoluteUri) &&
!(OperatingSystem.IsIOSVersionAtLeast(11) || OperatingSystem.IsTvOSVersionAtLeast(11)))
{
return;
}
await handler.SyncPlatformCookiesAsync(safeFullUri.AbsoluteUri);
}
LoadRequest(request);
}
catch (UriFormatException formatException)
{
// If we got a format exception trying to parse the URI, it might be because
// someone is passing in a local bundled file page. If we can find a better way
// to detect that scenario, we should use it; until then, we'll fall back to
// local file loading here and see if that works:
if (!string.IsNullOrEmpty(url))
{
if (!LoadFile(url))
{
if (_handler.TryGetTarget(out var handler))
handler.MauiContext?.CreateLogger<MauiWKWebView>()?.LogWarning($"Unable to Load Url {url}: {formatException}");
}
}
}
catch (Exception exc)
{
if (_handler.TryGetTarget(out var handler))
handler.MauiContext?.CreateLogger<MauiWKWebView>()?.LogWarning($"Unable to Load Url {url}: {exc}");
}
}
public void LoadUrl(string? url)
{
LoadUrlAsync(url).FireAndForget();
}
// https://developer.apple.com/forums/thread/99674
// WKWebView and making sure cookies synchronize is really quirky
// The main workaround I've found for ensuring that cookies synchronize
// is to share the Process Pool between all WkWebView instances.
// It also has to be shared at the point you call init
public static WKWebViewConfiguration CreateConfiguration()
{
// By default, setting inline media playback to allowed, including autoplay
// and picture in picture, since these things MUST be set during the webview
// creation, and have no effect if set afterwards.
// A custom handler factory delegate could be set to disable these defaults
// but if we do not set them here, they cannot be changed once the
// handler's platform view is created, so erring on the side of wanting this
// capability by default.
var config = new WKWebViewConfiguration();
if (OperatingSystem.IsMacCatalystVersionAtLeast(10) || OperatingSystem.IsIOSVersionAtLeast(10))
{
config.AllowsPictureInPictureMediaPlayback = true;
config.AllowsInlineMediaPlayback = true;
config.MediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypes.None;
}
if (SharedPool == null)
SharedPool = config.ProcessPool;
else
config.ProcessPool = SharedPool;
return config;
}
bool LoadFile(string url)
{
try
{
var file = Path.GetFileNameWithoutExtension(url);
var ext = Path.GetExtension(url);
var nsUrl = NSBundle.MainBundle.GetUrlForResource(file, ext);
if (nsUrl == null)
{
return false;
}
LoadFileUrl(nsUrl, nsUrl);
return true;
}
catch (Exception ex)
{
if (_handler.TryGetTarget(out var handler))
handler.MauiContext?.CreateLogger<MauiWKWebView>()?.LogWarning($"Could not load {url} as local file: {ex}");
}
return false;
}
[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = IUIViewLifeCycleEvents.UnconditionalSuppressMessage)]
EventHandler? _movedToWindow;
event EventHandler IUIViewLifeCycleEvents.MovedToWindow
{
add => _movedToWindow += value;
remove => _movedToWindow -= value;
}
}
} |