diff --git a/Xamarin.Forms.ControlGallery.iOS/Properties/AssemblyInfo.cs b/Xamarin.Forms.ControlGallery.iOS/Properties/AssemblyInfo.cs index 78e7d1d81..7323d6df2 100644 --- a/Xamarin.Forms.ControlGallery.iOS/Properties/AssemblyInfo.cs +++ b/Xamarin.Forms.ControlGallery.iOS/Properties/AssemblyInfo.cs @@ -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))] \ No newline at end of file +[assembly: ExportImageSourceHandler(typeof(FailImageSource), typeof(BrokenImageSourceHandler))] +[assembly: ExportRenderer(typeof(WkWebView), typeof(Xamarin.Forms.Platform.iOS.WkWebViewRenderer))] diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue1666.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue1666.cs new file mode 100644 index 000000000..2220d74a9 --- /dev/null +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue1666.cs @@ -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 = $"Link"; + + 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 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; + } + } +} \ No newline at end of file diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems index 662ec1c0c..28e2831d0 100644 --- a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems @@ -478,6 +478,7 @@ + diff --git a/Xamarin.Forms.Platform.iOS/Renderers/WkWebViewRenderer.cs b/Xamarin.Forms.Platform.iOS/Renderers/WkWebViewRenderer.cs new file mode 100644 index 000000000..ed3d5a74f --- /dev/null +++ b/Xamarin.Forms.Platform.iOS/Renderers/WkWebViewRenderer.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 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 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 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.RegisterEffect(effect, this, NativeView); + } + } +} \ No newline at end of file diff --git a/Xamarin.Forms.Platform.iOS/Xamarin.Forms.Platform.iOS.csproj b/Xamarin.Forms.Platform.iOS/Xamarin.Forms.Platform.iOS.csproj index efd04d92b..f6e8baf2f 100644 --- a/Xamarin.Forms.Platform.iOS/Xamarin.Forms.Platform.iOS.csproj +++ b/Xamarin.Forms.Platform.iOS/Xamarin.Forms.Platform.iOS.csproj @@ -127,6 +127,7 @@ + True True