using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Foundation; using UIKit; using RectangleF = CoreGraphics.CGRect; namespace Xamarin.Forms.Platform.iOS { public class Platform : BindableObject, IPlatform, INavigation, IDisposable { internal static readonly BindableProperty RendererProperty = BindableProperty.CreateAttached("Renderer", typeof(IVisualElementRenderer), typeof(Platform), default(IVisualElementRenderer), propertyChanged: (bindable, oldvalue, newvalue) => { var view = bindable as VisualElement; if (view != null) view.IsPlatformEnabled = newvalue != null; }); readonly int _alertPadding = 10; readonly List _modals; readonly PlatformRenderer _renderer; bool _animateModals = true; bool _appeared; bool _disposed; internal Platform() { _renderer = new PlatformRenderer(this); _modals = new List(); var busyCount = 0; MessagingCenter.Subscribe(this, Page.BusySetSignalName, (Page sender, bool enabled) => { if (!PageIsChildOfPlatform(sender)) return; busyCount = Math.Max(0, enabled ? busyCount + 1 : busyCount - 1); UIApplication.SharedApplication.NetworkActivityIndicatorVisible = busyCount > 0; }); MessagingCenter.Subscribe(this, Page.AlertSignalName, (Page sender, AlertArguments arguments) => { if (!PageIsChildOfPlatform(sender)) return; if (Forms.IsiOS8OrNewer) { var alert = UIAlertController.Create(arguments.Title, arguments.Message, UIAlertControllerStyle.Alert); var oldFrame = alert.View.Frame; alert.View.Frame = new RectangleF(oldFrame.X, oldFrame.Y, oldFrame.Width, oldFrame.Height - _alertPadding * 2); alert.AddAction(UIAlertAction.Create(arguments.Cancel, UIAlertActionStyle.Cancel, a => arguments.SetResult(false))); if (arguments.Accept != null) alert.AddAction(UIAlertAction.Create(arguments.Accept, UIAlertActionStyle.Default, a => arguments.SetResult(true))); var page = _modals.Any() ? _modals.Last() : Page; var vc = GetRenderer(page).ViewController; vc.PresentViewController(alert, true, null); } else { UIAlertView alertView; if (arguments.Accept != null) alertView = new UIAlertView(arguments.Title, arguments.Message, null, arguments.Cancel, arguments.Accept); else alertView = new UIAlertView(arguments.Title, arguments.Message, null, arguments.Cancel); alertView.Dismissed += (o, args) => arguments.SetResult(args.ButtonIndex != 0); alertView.Show(); } }); MessagingCenter.Subscribe(this, Page.ActionSheetSignalName, (Page sender, ActionSheetArguments arguments) => { if (!PageIsChildOfPlatform(sender)) return; var pageRoot = sender; while (!Application.IsApplicationOrNull(pageRoot.RealParent)) pageRoot = (Page)pageRoot.RealParent; var pageRenderer = GetRenderer(pageRoot); if (Forms.IsiOS8OrNewer) { var alert = UIAlertController.Create(arguments.Title, null, UIAlertControllerStyle.ActionSheet); if (arguments.Cancel != null) { alert.AddAction(UIAlertAction.Create(arguments.Cancel, UIAlertActionStyle.Cancel, a => arguments.SetResult(arguments.Cancel))); } if (arguments.Destruction != null) { alert.AddAction(UIAlertAction.Create(arguments.Destruction, UIAlertActionStyle.Destructive, a => arguments.SetResult(arguments.Destruction))); } foreach (var label in arguments.Buttons) { if (label == null) continue; var blabel = label; alert.AddAction(UIAlertAction.Create(blabel, UIAlertActionStyle.Default, a => arguments.SetResult(blabel))); } if (UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Pad) { UIDevice.CurrentDevice.BeginGeneratingDeviceOrientationNotifications(); var observer = NSNotificationCenter.DefaultCenter.AddObserver(UIDevice.OrientationDidChangeNotification, n => { alert.PopoverPresentationController.SourceRect = pageRenderer.ViewController.View.Bounds; }); arguments.Result.Task.ContinueWith(t => { NSNotificationCenter.DefaultCenter.RemoveObserver(observer); UIDevice.CurrentDevice.EndGeneratingDeviceOrientationNotifications(); }, TaskScheduler.FromCurrentSynchronizationContext()); alert.PopoverPresentationController.SourceView = pageRenderer.ViewController.View; alert.PopoverPresentationController.SourceRect = pageRenderer.ViewController.View.Bounds; alert.PopoverPresentationController.PermittedArrowDirections = 0; // No arrow } pageRenderer.ViewController.PresentViewController(alert, true, null); } else { var actionSheet = new UIActionSheet(arguments.Title, null, arguments.Cancel, arguments.Destruction, arguments.Buttons.ToArray()); actionSheet.ShowInView(pageRenderer.NativeView); actionSheet.Clicked += (o, args) => { string title = null; if (args.ButtonIndex != -1) title = actionSheet.ButtonTitle(args.ButtonIndex); // iOS 8 always calls WillDismiss twice, once with the real result, and again with -1. arguments.Result.TrySetResult(title); }; } }); } internal UIViewController ViewController { get { return _renderer; } } Page Page { get; set; } Application TargetApplication { get { if (Page == null) return null; return Page.RealParent as Application; } } void IDisposable.Dispose() { if (_disposed) return; _disposed = true; Page.DescendantRemoved -= HandleChildRemoved; MessagingCenter.Unsubscribe(this, Page.ActionSheetSignalName); MessagingCenter.Unsubscribe(this, Page.AlertSignalName); MessagingCenter.Unsubscribe(this, Page.BusySetSignalName); DisposeModelAndChildrenRenderers(Page); foreach (var modal in _modals) DisposeModelAndChildrenRenderers(modal); _renderer.Dispose(); } void INavigation.InsertPageBefore(Page page, Page before) { throw new InvalidOperationException("InsertPageBefore is not supported globally on iOS, please use a NavigationPage."); } IReadOnlyList INavigation.ModalStack { get { return _modals; } } IReadOnlyList INavigation.NavigationStack { get { return new List(); } } Task INavigation.PopAsync() { return ((INavigation)this).PopAsync(true); } Task INavigation.PopAsync(bool animated) { throw new InvalidOperationException("PopAsync is not supported globally on iOS, please use a NavigationPage."); } Task INavigation.PopModalAsync() { return ((INavigation)this).PopModalAsync(true); } async Task INavigation.PopModalAsync(bool animated) { var modal = _modals.Last(); _modals.Remove(modal); modal.DescendantRemoved -= HandleChildRemoved; var controller = GetRenderer(modal) as UIViewController; if (_modals.Count >= 1 && controller != null) await controller.DismissViewControllerAsync(animated); else await _renderer.DismissViewControllerAsync(animated); DisposeModelAndChildrenRenderers(modal); return modal; } Task INavigation.PopToRootAsync() { return ((INavigation)this).PopToRootAsync(true); } Task INavigation.PopToRootAsync(bool animated) { throw new InvalidOperationException("PopToRootAsync is not supported globally on iOS, please use a NavigationPage."); } Task INavigation.PushAsync(Page root) { return ((INavigation)this).PushAsync(root, true); } Task INavigation.PushAsync(Page root, bool animated) { throw new InvalidOperationException("PushAsync is not supported globally on iOS, please use a NavigationPage."); } Task INavigation.PushModalAsync(Page modal) { return ((INavigation)this).PushModalAsync(modal, true); } Task INavigation.PushModalAsync(Page modal, bool animated) { _modals.Add(modal); modal.Platform = this; modal.DescendantRemoved += HandleChildRemoved; if (_appeared) return PresentModal(modal, _animateModals && animated); return Task.FromResult(null); } void INavigation.RemovePage(Page page) { throw new InvalidOperationException("RemovePage is not supported globally on iOS, please use a NavigationPage."); } SizeRequest IPlatform.GetNativeSize(VisualElement view, double widthConstraint, double heightConstraint) { var renderView = GetRenderer(view); if (renderView == null || renderView.NativeView == null) return new SizeRequest(Size.Zero); return renderView.GetDesiredSize(widthConstraint, heightConstraint); } public static IVisualElementRenderer CreateRenderer(VisualElement element) { var t = element.GetType(); var renderer = Registrar.Registered.GetHandler(t) ?? new DefaultRenderer(); renderer.SetElement(element); return renderer; } public static IVisualElementRenderer GetRenderer(VisualElement bindable) { return (IVisualElementRenderer)bindable.GetValue(RendererProperty); } public static void SetRenderer(VisualElement bindable, IVisualElementRenderer value) { bindable.SetValue(RendererProperty, value); } protected override void OnBindingContextChanged() { SetInheritedBindingContext(Page, BindingContext); base.OnBindingContextChanged(); } internal void DidAppear() { _animateModals = false; TargetApplication.NavigationProxy.Inner = this; _animateModals = true; } internal void DisposeModelAndChildrenRenderers(Element view) { IVisualElementRenderer renderer; foreach (VisualElement child in view.Descendants()) { renderer = GetRenderer(child); child.ClearValue(RendererProperty); if (renderer != null) { renderer.NativeView.RemoveFromSuperview(); renderer.Dispose(); } } renderer = GetRenderer((VisualElement)view); if (renderer != null) { if (renderer.ViewController != null) { var modalWrapper = renderer.ViewController.ParentViewController as ModalWrapper; if (modalWrapper != null) modalWrapper.Dispose(); } renderer.NativeView.RemoveFromSuperview(); renderer.Dispose(); } view.ClearValue(RendererProperty); } internal void DisposeRendererAndChildren(IVisualElementRenderer rendererToRemove) { if (rendererToRemove == null) return; if (rendererToRemove.Element != null && GetRenderer(rendererToRemove.Element) == rendererToRemove) rendererToRemove.Element.ClearValue(RendererProperty); var subviews = rendererToRemove.NativeView.Subviews; for (var i = 0; i < subviews.Length; i++) { var childRenderer = subviews[i] as IVisualElementRenderer; if (childRenderer != null) DisposeRendererAndChildren(childRenderer); } rendererToRemove.NativeView.RemoveFromSuperview(); rendererToRemove.Dispose(); } internal void LayoutSubviews() { if (Page == null) return; var rootRenderer = GetRenderer(Page); if (rootRenderer == null) return; rootRenderer.SetElementSize(new Size(_renderer.View.Bounds.Width, _renderer.View.Bounds.Height)); } internal void SetPage(Page newRoot) { if (newRoot == null) return; if (Page != null) throw new NotImplementedException(); Page = newRoot; if (_appeared == false) return; Page.Platform = this; AddChild(Page); Page.DescendantRemoved += HandleChildRemoved; TargetApplication.NavigationProxy.Inner = this; } internal void WillAppear() { if (_appeared) return; _renderer.View.BackgroundColor = UIColor.White; _renderer.View.ContentMode = UIViewContentMode.Redraw; Page.Platform = this; AddChild(Page); Page.DescendantRemoved += HandleChildRemoved; _appeared = true; } void AddChild(VisualElement view) { if (!Application.IsApplicationOrNull(view.RealParent)) Console.Error.WriteLine("Tried to add parented view to canvas directly"); if (GetRenderer(view) == null) { var viewRenderer = CreateRenderer(view); SetRenderer(view, viewRenderer); _renderer.View.AddSubview(viewRenderer.NativeView); if (viewRenderer.ViewController != null) _renderer.AddChildViewController(viewRenderer.ViewController); viewRenderer.NativeView.Frame = new RectangleF(0, 0, _renderer.View.Bounds.Width, _renderer.View.Bounds.Height); viewRenderer.SetElementSize(new Size(_renderer.View.Bounds.Width, _renderer.View.Bounds.Height)); } else Console.Error.WriteLine("Potential view double add"); } void HandleChildRemoved(object sender, ElementEventArgs e) { var view = e.Element; DisposeModelAndChildrenRenderers(view); } bool PageIsChildOfPlatform(Page page) { while (!Application.IsApplicationOrNull(page.RealParent)) page = (Page)page.RealParent; return Page == page || _modals.Contains(page); } async Task PresentModal(Page modal, bool animated) { var modalRenderer = GetRenderer(modal); if (modalRenderer == null) { modalRenderer = CreateRenderer(modal); SetRenderer(modal, modalRenderer); } var wrapper = new ModalWrapper(modalRenderer); if (_modals.Count > 1) { var topPage = _modals[_modals.Count - 2]; var controller = GetRenderer(topPage) as UIViewController; if (controller != null) { await controller.PresentViewControllerAsync(wrapper, animated); await Task.Delay(5); return; } } // One might wonder why these delays are here... well thats a great question. It turns out iOS will claim the // presentation is complete before it really is. It does not however inform you when it is really done (and thus // would be safe to dismiss the VC). Fortunately this is almost never an issue await _renderer.PresentViewControllerAsync(wrapper, animated); await Task.Delay(5); } internal class DefaultRenderer : VisualElementRenderer { } } }