[iOS] WebView renderer that replaces UIWebView with WkWebView (#3346)
* wkwebview * Remove default UITest * PR changes
This commit is contained in:
Родитель
c9ef84345d
Коммит
1ee8e7d577
|
@ -40,4 +40,5 @@ using Xamarin.Forms.Controls;
|
|||
[assembly: Xamarin.Forms.ResolutionGroupName (Xamarin.Forms.Controls.Issues.Effects.ResolutionGroupName)]
|
||||
|
||||
// Deliberately broken image source and handler so we can test handling of image loading errors
|
||||
[assembly: ExportImageSourceHandler(typeof(FailImageSource), typeof(BrokenImageSourceHandler))]
|
||||
[assembly: ExportImageSourceHandler(typeof(FailImageSource), typeof(BrokenImageSourceHandler))]
|
||||
[assembly: ExportRenderer(typeof(WkWebView), typeof(Xamarin.Forms.Platform.iOS.WkWebViewRenderer))]
|
||||
|
|
|
@ -0,0 +1,149 @@
|
|||
using System;
|
||||
using Xamarin.Forms;
|
||||
using Xamarin.Forms.CustomAttributes;
|
||||
using Xamarin.Forms.Internals;
|
||||
|
||||
#if UITEST
|
||||
using Xamarin.Forms.Core.UITests;
|
||||
using Xamarin.UITest;
|
||||
using NUnit.Framework;
|
||||
#endif
|
||||
|
||||
public class WkWebView : WebView { }
|
||||
|
||||
namespace Xamarin.Forms.Controls.Issues
|
||||
{
|
||||
|
||||
#if UITEST
|
||||
[Category(UITestCategories.ManualReview)]
|
||||
#endif
|
||||
[Preserve(AllMembers = true)]
|
||||
[Issue(IssueTracker.Github, 1666, "Use WKWebView on iOS", PlatformAffected.iOS)]
|
||||
public class Issue1666 : TestContentPage // or TestMasterDetailPage, etc ...
|
||||
{
|
||||
protected override void Init()
|
||||
{
|
||||
var buttonBack = new Button() { Text = "<", BackgroundColor = Color.LightBlue, AutomationId = "buttonBack" };
|
||||
var buttonNext = new Button() { Text = ">", BackgroundColor = Color.LightBlue, AutomationId = "buttonNext" };
|
||||
var buttonClear = new Button() { Text = "--", BackgroundColor = Color.LightBlue, AutomationId = "buttonClear" };
|
||||
var buttonState = new Button() { Text = "?", BackgroundColor = Color.LightBlue, AutomationId = "buttonState" };
|
||||
var buttonStop = new Button() { Text = "X", BackgroundColor = Color.LightBlue, AutomationId = "buttonStop" };
|
||||
|
||||
var buttonA = new Button() { Text = "GO", BackgroundColor = Color.LightBlue, AutomationId = "buttonA" };
|
||||
var buttonB = new Button() { Text = "HTML", BackgroundColor = Color.LightBlue, AutomationId = "buttonB" };
|
||||
var buttonC = new Button() { Text = "EVAL", BackgroundColor = Color.LightBlue, AutomationId = "buttonC" };
|
||||
var buttonD = new Button() { Text = "AEVAL", BackgroundColor = Color.LightBlue, AutomationId = "buttonD" };
|
||||
|
||||
var url = "https://www.microsoft.com/";
|
||||
var html = $"<html><body><a href=\"{url}\">Link</a></body></html>";
|
||||
|
||||
var webView = new WkWebView()
|
||||
{
|
||||
HeightRequest = 40,
|
||||
Source = new HtmlWebViewSource { Html = html }
|
||||
};
|
||||
|
||||
var vcr = new Grid();
|
||||
vcr.Children.AddHorizontal(new[] { buttonBack, buttonNext, buttonClear, buttonState, buttonStop });
|
||||
|
||||
var evals = new Grid();
|
||||
evals.Children.AddHorizontal(new[] { buttonA, buttonB, buttonC, buttonD });
|
||||
|
||||
var entry = new Entry() { AutomationId = "entry" };
|
||||
entry.BackgroundColor = Color.Wheat;
|
||||
|
||||
var buttons = new Grid();
|
||||
buttons.Children.AddVertical(vcr);
|
||||
buttons.Children.AddVertical(evals);
|
||||
buttons.Children.AddVertical(entry);
|
||||
|
||||
var console = new Label()
|
||||
{
|
||||
AutomationId = "console",
|
||||
Text = "Loaded\n"
|
||||
};
|
||||
Action<string> log = s => { console.Text = s + "\n" + console.Text; };
|
||||
|
||||
var grid = new Grid();
|
||||
grid.Children.AddVertical(webView);
|
||||
grid.Children.AddVertical(buttons);
|
||||
grid.Children.AddVertical(new ScrollView() { Content = console });
|
||||
|
||||
buttonA.Clicked += (s, e) =>
|
||||
{
|
||||
webView.Source = new UrlWebViewSource() { Url = url };
|
||||
};
|
||||
|
||||
buttonB.Clicked += (s, e) => {
|
||||
webView.Source = new HtmlWebViewSource()
|
||||
{
|
||||
Html = html
|
||||
};
|
||||
};
|
||||
|
||||
var js = "1 + 2";
|
||||
buttonC.Clicked += (s, e) => {
|
||||
log($"Eval: {js}");
|
||||
webView.Eval(js);
|
||||
};
|
||||
|
||||
webView.EvalRequested += (s, e) =>
|
||||
{
|
||||
log($"EvalRequested: {e.Script}");
|
||||
};
|
||||
|
||||
buttonD.Clicked += (s, e) => {
|
||||
log($"AEval: {js}");
|
||||
var promise = webView.EvaluateJavaScriptAsync(js);
|
||||
promise.ContinueWith(a => Device.BeginInvokeOnMainThread(() => log($"Evaled: {a.Result}")));
|
||||
};
|
||||
|
||||
bool cancel = false;
|
||||
buttonNext.Clicked += (s, e) => { webView.GoForward(); log($"GoForward: {webView.CanGoBack}/{webView.CanGoForward}"); };
|
||||
buttonBack.Clicked += (s, e) => { webView.GoBack(); log($"GoBack: {webView.CanGoBack}/{webView.CanGoForward}"); };
|
||||
buttonClear.Clicked += (s, e) => { console.Text = ""; };
|
||||
buttonStop.Clicked += (s, e) => { cancel = true; log("Cancelling navigation"); };
|
||||
buttonState.Clicked += (s, e) => {
|
||||
log($"F/B: {webView.CanGoBack}/{webView.CanGoForward}");
|
||||
log($"Source: {webView.Source.ToString()}");
|
||||
};
|
||||
|
||||
bool navigating = false;
|
||||
webView.Navigating += (s, e) =>
|
||||
{
|
||||
entry.Text = e.Url;
|
||||
entry.BackgroundColor = Color.LightPink;
|
||||
|
||||
if (!navigating)
|
||||
{
|
||||
log("Navigating");
|
||||
navigating = true;
|
||||
}
|
||||
|
||||
if (cancel)
|
||||
{
|
||||
e.Cancel = true;
|
||||
log("Cancel navigation");
|
||||
cancel = false;
|
||||
}
|
||||
};
|
||||
|
||||
webView.Navigated += (s, e) =>
|
||||
{
|
||||
var text = $"Navigated {e.NavigationEvent}, ";
|
||||
text += $"Result: {e.Result}";
|
||||
log(text);
|
||||
|
||||
entry.Text = e.Url;
|
||||
entry.BackgroundColor = Color.LightBlue;
|
||||
|
||||
cancel = false;
|
||||
navigating = false;
|
||||
};
|
||||
|
||||
// Initialize ui here instead of ctor
|
||||
Content = grid;
|
||||
BackgroundColor = Color.Gray;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -478,6 +478,7 @@
|
|||
<Compile Include="$(MSBuildThisFileDirectory)Issue2837.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Issue2740.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Issue1939.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Issue1666.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Issue2838.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)_Template.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Bugzilla56298.cs" />
|
||||
|
|
|
@ -0,0 +1,289 @@
|
|||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Threading.Tasks;
|
||||
using Foundation;
|
||||
using UIKit;
|
||||
using WebKit;
|
||||
using Xamarin.Forms.Internals;
|
||||
using Uri = System.Uri;
|
||||
|
||||
namespace Xamarin.Forms.Platform.iOS
|
||||
{
|
||||
public class WkWebViewRenderer : WKWebView, IVisualElementRenderer, IWebViewDelegate, IEffectControlProvider
|
||||
{
|
||||
EventTracker _events;
|
||||
bool _ignoreSourceChanges;
|
||||
WebNavigationEvent _lastBackForwardEvent;
|
||||
VisualElementPackager _packager;
|
||||
#pragma warning disable 0414
|
||||
VisualElementTracker _tracker;
|
||||
#pragma warning restore 0414
|
||||
public WkWebViewRenderer() : base(RectangleF.Empty, new WKWebViewConfiguration())
|
||||
{
|
||||
}
|
||||
|
||||
WebView WebView => Element as WebView;
|
||||
|
||||
public VisualElement Element { get; private set; }
|
||||
|
||||
public event EventHandler<VisualElementChangedEventArgs> ElementChanged;
|
||||
|
||||
public SizeRequest GetDesiredSize(double widthConstraint, double heightConstraint)
|
||||
{
|
||||
return NativeView.GetSizeRequest(widthConstraint, heightConstraint, 44, 44);
|
||||
}
|
||||
|
||||
public void SetElement(VisualElement element)
|
||||
{
|
||||
var oldElement = Element;
|
||||
Element = element;
|
||||
Element.PropertyChanged += HandlePropertyChanged;
|
||||
WebView.EvalRequested += OnEvalRequested;
|
||||
WebView.EvaluateJavaScriptRequested += OnEvaluateJavaScriptRequested;
|
||||
WebView.GoBackRequested += OnGoBackRequested;
|
||||
WebView.GoForwardRequested += OnGoForwardRequested;
|
||||
NavigationDelegate = new CustomWebViewDelegate(this);
|
||||
|
||||
BackgroundColor = UIColor.Clear;
|
||||
|
||||
AutosizesSubviews = true;
|
||||
|
||||
_tracker = new VisualElementTracker(this);
|
||||
|
||||
_packager = new VisualElementPackager(this);
|
||||
_packager.Load();
|
||||
|
||||
_events = new EventTracker(this);
|
||||
_events.LoadEvents(this);
|
||||
|
||||
Load();
|
||||
|
||||
OnElementChanged(new VisualElementChangedEventArgs(oldElement, element));
|
||||
|
||||
EffectUtilities.RegisterEffectControlProvider(this, oldElement, element);
|
||||
|
||||
if (Element != null && !string.IsNullOrEmpty(Element.AutomationId))
|
||||
AccessibilityIdentifier = Element.AutomationId;
|
||||
|
||||
if (element != null)
|
||||
element.SendViewInitialized(this);
|
||||
}
|
||||
|
||||
public void SetElementSize(Size size)
|
||||
{
|
||||
Layout.LayoutChildIntoBoundingRegion(Element, new Rectangle(Element.X, Element.Y, size.Width, size.Height));
|
||||
}
|
||||
|
||||
public void LoadHtml(string html, string baseUrl)
|
||||
{
|
||||
if (html != null)
|
||||
LoadHtmlString(html, baseUrl == null ? new NSUrl(NSBundle.MainBundle.BundlePath, true) : new NSUrl(baseUrl, true));
|
||||
}
|
||||
|
||||
public void LoadUrl(string url)
|
||||
{
|
||||
var uri = new Uri(url);
|
||||
var safeHostUri = new Uri($"{uri.Scheme}://{uri.Authority}", UriKind.Absolute);
|
||||
var safeRelativeUri = new Uri($"{uri.PathAndQuery}{uri.Fragment}", UriKind.Relative);
|
||||
LoadRequest(new NSUrlRequest(new Uri(safeHostUri, safeRelativeUri)));
|
||||
}
|
||||
|
||||
public override void LayoutSubviews()
|
||||
{
|
||||
base.LayoutSubviews();
|
||||
|
||||
// ensure that inner scrollview properly resizes when frame of webview updated
|
||||
ScrollView.Frame = Bounds;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
if (IsLoading)
|
||||
StopLoading();
|
||||
|
||||
Element.PropertyChanged -= HandlePropertyChanged;
|
||||
WebView.EvalRequested -= OnEvalRequested;
|
||||
WebView.EvaluateJavaScriptRequested -= OnEvaluateJavaScriptRequested;
|
||||
WebView.GoBackRequested -= OnGoBackRequested;
|
||||
WebView.GoForwardRequested -= OnGoForwardRequested;
|
||||
|
||||
_tracker?.Dispose();
|
||||
_packager?.Dispose();
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
protected virtual void OnElementChanged(VisualElementChangedEventArgs e)
|
||||
{
|
||||
var changed = ElementChanged;
|
||||
if (changed != null)
|
||||
changed(this, e);
|
||||
}
|
||||
|
||||
void HandlePropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == WebView.SourceProperty.PropertyName)
|
||||
Load();
|
||||
}
|
||||
|
||||
void Load()
|
||||
{
|
||||
if (_ignoreSourceChanges)
|
||||
return;
|
||||
|
||||
if (((WebView)Element).Source != null)
|
||||
((WebView)Element).Source.Load(this);
|
||||
|
||||
UpdateCanGoBackForward();
|
||||
}
|
||||
|
||||
void OnEvalRequested(object sender, EvalRequested eventArg)
|
||||
{
|
||||
EvaluateJavaScriptAsync(eventArg.Script);
|
||||
}
|
||||
|
||||
async Task<string> OnEvaluateJavaScriptRequested(string script)
|
||||
{
|
||||
var result = await EvaluateJavaScriptAsync(script);
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
void OnGoBackRequested(object sender, EventArgs eventArgs)
|
||||
{
|
||||
if (CanGoBack)
|
||||
{
|
||||
_lastBackForwardEvent = WebNavigationEvent.Back;
|
||||
GoBack();
|
||||
}
|
||||
|
||||
UpdateCanGoBackForward();
|
||||
}
|
||||
|
||||
void OnGoForwardRequested(object sender, EventArgs eventArgs)
|
||||
{
|
||||
if (CanGoForward)
|
||||
{
|
||||
_lastBackForwardEvent = WebNavigationEvent.Forward;
|
||||
GoForward();
|
||||
}
|
||||
|
||||
UpdateCanGoBackForward();
|
||||
}
|
||||
|
||||
void UpdateCanGoBackForward()
|
||||
{
|
||||
((IWebViewController)WebView).CanGoBack = CanGoBack;
|
||||
((IWebViewController)WebView).CanGoForward = CanGoForward;
|
||||
}
|
||||
|
||||
class CustomWebViewDelegate : WKNavigationDelegate
|
||||
{
|
||||
readonly WkWebViewRenderer _renderer;
|
||||
WebNavigationEvent _lastEvent;
|
||||
|
||||
public CustomWebViewDelegate(WkWebViewRenderer renderer)
|
||||
{
|
||||
if (renderer == null)
|
||||
throw new ArgumentNullException("renderer");
|
||||
_renderer = renderer;
|
||||
}
|
||||
|
||||
WebView WebView => _renderer.WebView;
|
||||
|
||||
public override void DidFailNavigation(WKWebView webView, WKNavigation navigation, NSError error)
|
||||
{
|
||||
var url = GetCurrentUrl();
|
||||
WebView.SendNavigated(
|
||||
new WebNavigatedEventArgs(_lastEvent, new UrlWebViewSource { Url = url }, url, WebNavigationResult.Failure)
|
||||
);
|
||||
|
||||
_renderer.UpdateCanGoBackForward();
|
||||
}
|
||||
|
||||
public override void DidFinishNavigation(WKWebView webView, WKNavigation navigation)
|
||||
{
|
||||
if (webView.IsLoading)
|
||||
return;
|
||||
|
||||
_renderer._ignoreSourceChanges = true;
|
||||
var url = GetCurrentUrl();
|
||||
WebView.SetValueFromRenderer(WebView.SourceProperty, new UrlWebViewSource { Url = url });
|
||||
_renderer._ignoreSourceChanges = false;
|
||||
|
||||
var args = new WebNavigatedEventArgs(_lastEvent, WebView.Source, url, WebNavigationResult.Success);
|
||||
WebView.SendNavigated(args);
|
||||
|
||||
_renderer.UpdateCanGoBackForward();
|
||||
}
|
||||
|
||||
public override void DidStartProvisionalNavigation(WKWebView webView, WKNavigation navigation)
|
||||
{
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/37509990/migrating-from-uiwebview-to-wkwebview
|
||||
public override void DecidePolicy(WKWebView webView, WKNavigationAction navigationAction, Action<WKNavigationActionPolicy> decisionHandler)
|
||||
{
|
||||
var navEvent = WebNavigationEvent.NewPage;
|
||||
var navigationType = navigationAction.NavigationType;
|
||||
switch (navigationType)
|
||||
{
|
||||
case WKNavigationType.LinkActivated:
|
||||
navEvent = WebNavigationEvent.NewPage;
|
||||
break;
|
||||
case WKNavigationType.FormSubmitted:
|
||||
navEvent = WebNavigationEvent.NewPage;
|
||||
break;
|
||||
case WKNavigationType.BackForward:
|
||||
navEvent = _renderer._lastBackForwardEvent;
|
||||
break;
|
||||
case WKNavigationType.Reload:
|
||||
navEvent = WebNavigationEvent.Refresh;
|
||||
break;
|
||||
case WKNavigationType.FormResubmitted:
|
||||
navEvent = WebNavigationEvent.NewPage;
|
||||
break;
|
||||
case WKNavigationType.Other:
|
||||
navEvent = WebNavigationEvent.NewPage;
|
||||
break;
|
||||
}
|
||||
|
||||
_lastEvent = navEvent;
|
||||
var request = navigationAction.Request;
|
||||
var lastUrl = request.Url.ToString();
|
||||
var args = new WebNavigatingEventArgs(navEvent, new UrlWebViewSource { Url = lastUrl }, lastUrl);
|
||||
|
||||
WebView.SendNavigating(args);
|
||||
_renderer.UpdateCanGoBackForward();
|
||||
decisionHandler(args.Cancel ? WKNavigationActionPolicy.Cancel : WKNavigationActionPolicy.Allow);
|
||||
}
|
||||
|
||||
string GetCurrentUrl()
|
||||
{
|
||||
return _renderer?.Url?.AbsoluteUrl?.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
#region IPlatformRenderer implementation
|
||||
|
||||
public UIView NativeView
|
||||
{
|
||||
get { return this; }
|
||||
}
|
||||
|
||||
public UIViewController ViewController
|
||||
{
|
||||
get { return null; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
void IEffectControlProvider.RegisterEffect(Effect effect)
|
||||
{
|
||||
VisualElementRenderer<VisualElement>.RegisterEffect(effect, this, NativeView);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -127,6 +127,7 @@
|
|||
<Compile Include="Renderers\AlignmentExtensions.cs" />
|
||||
<Compile Include="Forms.cs" />
|
||||
<Compile Include="PageExtensions.cs" />
|
||||
<Compile Include="Renderers\WkWebViewRenderer.cs" />
|
||||
<Compile Include="Resources\StringResources.Designer.cs">
|
||||
<AutoGen>True</AutoGen>
|
||||
<DesignTime>True</DesignTime>
|
||||
|
|
Загрузка…
Ссылка в новой задаче