[iOS] WebView renderer that replaces UIWebView with WkWebView (#3346)

* wkwebview

* Remove default UITest

* PR changes
This commit is contained in:
kingces95 2018-08-01 22:45:49 -10:00 коммит произвёл GitHub
Родитель c9ef84345d
Коммит 1ee8e7d577
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
5 изменённых файлов: 442 добавлений и 1 удалений

Просмотреть файл

@ -41,3 +41,4 @@ using Xamarin.Forms.Controls;
// Deliberately broken image source and handler so we can test handling of image loading errors
[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>