using CoreGraphics; using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Threading.Tasks; using ObjCRuntime; using UIKit; using Xamarin.Forms.Internals; using Xamarin.Forms.PlatformConfiguration.iOSSpecific; using static Xamarin.Forms.PlatformConfiguration.iOSSpecific.Page; using static Xamarin.Forms.PlatformConfiguration.iOSSpecific.NavigationPage; using PageUIStatusBarAnimation = Xamarin.Forms.PlatformConfiguration.iOSSpecific.UIStatusBarAnimation; using PointF = CoreGraphics.CGPoint; using RectangleF = CoreGraphics.CGRect; using SizeF = CoreGraphics.CGSize; namespace Xamarin.Forms.Platform.iOS { public class NavigationRenderer : UINavigationController, IVisualElementRenderer, IEffectControlProvider { internal const string UpdateToolbarButtons = "Xamarin.UpdateToolbarButtons"; bool _appeared; bool _ignorePopCall; bool _loaded; MasterDetailPage _parentMasterDetailPage; Size _queuedSize; UIViewController[] _removeControllers; UIToolbar _secondaryToolbar; VisualElementTracker _tracker; nfloat _navigationBottom = 0; bool _hasNavigationBar; UIImage _defaultNavBarShadowImage; UIImage _defaultNavBarBackImage; public NavigationRenderer() : base(typeof(FormsNavigationBar), null) { MessagingCenter.Subscribe(this, UpdateToolbarButtons, sender => { if (!ViewControllers.Any()) return; var parentingViewController = (ParentingViewController)ViewControllers.Last(); parentingViewController?.UpdateLeftBarButtonItem(); }); } Page Current { get; set; } IPageController PageController => Element as IPageController; NavigationPage NavPage => Element as NavigationPage; public VisualElement Element { get; private set; } public event EventHandler ElementChanged; public SizeRequest GetDesiredSize(double widthConstraint, double heightConstraint) { return NativeView.GetSizeRequest(widthConstraint, heightConstraint); } public UIView NativeView { get { return View; } } public void SetElement(VisualElement element) { var oldElement = Element; Element = element; OnElementChanged(new VisualElementChangedEventArgs(oldElement, element)); if (element != null) element.SendViewInitialized(NativeView); EffectUtilities.RegisterEffectControlProvider(this, oldElement, element); } public void SetElementSize(Size size) { if (_loaded) Element.Layout(new Rectangle(Element.X, Element.Y, size.Width, size.Height)); else _queuedSize = size; } public UIViewController ViewController { get { return this; } } //TODO: this was deprecated in iOS8.0 and is not called in 9.0+ public override void DidRotate(UIInterfaceOrientation fromInterfaceOrientation) { base.DidRotate(fromInterfaceOrientation); View.SetNeedsLayout(); var parentingViewController = (ParentingViewController)ViewControllers.Last(); parentingViewController?.UpdateLeftBarButtonItem(); } public Task PopToRootAsync(Page page, bool animated = true) { return OnPopToRoot(page, animated); } public override UIViewController[] PopToRootViewController(bool animated) { if (!_ignorePopCall && ViewControllers.Length > 1) RemoveViewControllers(animated); return base.PopToRootViewController(animated); } public Task PopViewAsync(Page page, bool animated = true) { return OnPopViewAsync(page, animated); } public override UIViewController PopViewController(bool animated) { RemoveViewControllers(animated); return base.PopViewController(animated); } public Task PushPageAsync(Page page, bool animated = true) { return OnPushAsync(page, animated); } public override void ViewDidAppear(bool animated) { if (!_appeared) { _appeared = true; PageController?.SendAppearing(); } base.ViewDidAppear(animated); View.SetNeedsLayout(); } public override void ViewWillAppear(bool animated) { base.ViewWillAppear(animated); SetStatusBarStyle(); } public override void ViewDidDisappear(bool animated) { base.ViewDidDisappear(animated); if (!_appeared || Element == null) return; _appeared = false; PageController.SendDisappearing(); } public override void ViewDidLayoutSubviews() { base.ViewDidLayoutSubviews(); if (Current == null) return; UpdateToolBarVisible(); var navBarFrameBottom = Math.Min(NavigationBar.Frame.Bottom, 140); _navigationBottom = (nfloat)navBarFrameBottom; var toolbar = _secondaryToolbar; //save the state of the Current page we are calculating, this will fire before Current is updated _hasNavigationBar = NavigationPage.GetHasNavigationBar(Current); // Use 0 if the NavBar is hidden or will be hidden var toolbarY = NavigationBarHidden || NavigationBar.Translucent || !_hasNavigationBar ? 0 : navBarFrameBottom; toolbar.Frame = new RectangleF(0, toolbarY, View.Frame.Width, toolbar.Frame.Height); double trueBottom = toolbar.Hidden ? toolbarY : toolbar.Frame.Bottom; var modelSize = _queuedSize.IsZero ? Element.Bounds.Size : _queuedSize; PageController.ContainerArea = new Rectangle(0, toolbar.Hidden ? 0 : toolbar.Frame.Height, modelSize.Width, modelSize.Height - trueBottom); if (!_queuedSize.IsZero) { Element.Layout(new Rectangle(Element.X, Element.Y, _queuedSize.Width, _queuedSize.Height)); _queuedSize = Size.Zero; } _loaded = true; foreach (var view in View.Subviews) { if (view == NavigationBar || view == _secondaryToolbar) continue; view.Frame = View.Bounds; } } public override void ViewDidLoad() { base.ViewDidLoad(); UpdateTranslucent(); _secondaryToolbar = new SecondaryToolbar { Frame = new RectangleF(0, 0, 320, 44) }; View.Add(_secondaryToolbar); _secondaryToolbar.Hidden = true; FindParentMasterDetail(); var navPage = NavPage; if (navPage.CurrentPage == null) { throw new InvalidOperationException( "NavigationPage must have a root Page before being used. Either call PushAsync with a valid Page, or pass a Page to the constructor before usage."); } navPage.PushRequested += OnPushRequested; navPage.PopRequested += OnPopRequested; navPage.PopToRootRequested += OnPopToRootRequested; navPage.RemovePageRequested += OnRemovedPageRequested; navPage.InsertPageBeforeRequested += OnInsertPageBeforeRequested; UpdateTint(); UpdateBarBackgroundColor(); UpdateBarTextColor(); UpdateUseLargeTitles(); UpdateHideNavigationBarSeparator(); // If there is already stuff on the stack we need to push it navPage.Pages.ForEach(async p => await PushPageAsync(p, false)); _tracker = new VisualElementTracker(this); Element.PropertyChanged += HandlePropertyChanged; UpdateToolBarVisible(); UpdateBackgroundColor(); Current = navPage.CurrentPage; } protected override void Dispose(bool disposing) { if (disposing) { MessagingCenter.Unsubscribe(this, UpdateToolbarButtons); foreach (var childViewController in ViewControllers) childViewController.Dispose(); if (_tracker != null) _tracker.Dispose(); _secondaryToolbar.RemoveFromSuperview(); _secondaryToolbar.Dispose(); _secondaryToolbar = null; _parentMasterDetailPage = null; Current = null; // unhooks events var navPage = NavPage; navPage.PropertyChanged -= HandlePropertyChanged; navPage.PushRequested -= OnPushRequested; navPage.PopRequested -= OnPopRequested; navPage.PopToRootRequested -= OnPopToRootRequested; navPage.RemovePageRequested -= OnRemovedPageRequested; navPage.InsertPageBeforeRequested -= OnInsertPageBeforeRequested; } base.Dispose(disposing); if (_appeared) { PageController.SendDisappearing(); _appeared = false; } } protected virtual void OnElementChanged(VisualElementChangedEventArgs e) { ElementChanged?.Invoke(this, e); } protected virtual async Task OnPopToRoot(Page page, bool animated) { _ignorePopCall = true; var renderer = Platform.GetRenderer(page); if (renderer == null || renderer.ViewController == null) return false; var task = GetAppearedOrDisappearedTask(page); PopToRootViewController(animated); _ignorePopCall = false; var success = !await task; UpdateToolBarVisible(); return success; } protected virtual async Task OnPopViewAsync(Page page, bool animated) { if (_ignorePopCall) return true; var renderer = Platform.GetRenderer(page); if (renderer == null || renderer.ViewController == null) return false; var actuallyRemoved = false; if (page != ((ParentingViewController)TopViewController).Child) throw new NotSupportedException("Popped page does not appear on top of current navigation stack, please file a bug."); var task = GetAppearedOrDisappearedTask(page); UIViewController poppedViewController; poppedViewController = base.PopViewController(animated); actuallyRemoved = (poppedViewController == null) ? true : !await task; poppedViewController?.Dispose(); UpdateToolBarVisible(); return actuallyRemoved; } protected virtual async Task OnPushAsync(Page page, bool animated) { if (page is MasterDetailPage) System.Diagnostics.Trace.WriteLine($"Pushing a {nameof(MasterDetailPage)} onto a {nameof(NavigationPage)} is not a supported UI pattern on iOS. " + "Please see https://developer.apple.com/documentation/uikit/uisplitviewcontroller for more details."); var pack = CreateViewControllerForPage(page); var task = GetAppearedOrDisappearedTask(page); PushViewController(pack, animated); var shown = await task; UpdateToolBarVisible(); return shown; } ParentingViewController CreateViewControllerForPage(Page page) { if (Platform.GetRenderer(page) == null) Platform.SetRenderer(page, Platform.CreateRenderer(page)); // must pack into container so padding can work // otherwise the view controller is forced to 0,0 var pack = new ParentingViewController(this) { Child = page }; pack.UpdateTitleArea(page); var pageRenderer = Platform.GetRenderer(page); pack.View.AddSubview(pageRenderer.ViewController.View); pack.AddChildViewController(pageRenderer.ViewController); pageRenderer.ViewController.DidMoveToParentViewController(pack); return pack; } void FindParentMasterDetail() { Page page = Element as Page; var parentPages = page.GetParentPages(); var masterDetail = parentPages.OfType().FirstOrDefault(); if (masterDetail != null && parentPages.Append((Page)Element).Contains(masterDetail.Detail)) _parentMasterDetailPage = masterDetail; } Task GetAppearedOrDisappearedTask(Page page) { var tcs = new TaskCompletionSource(); var parentViewController = Platform.GetRenderer(page).ViewController.ParentViewController as ParentingViewController; if (parentViewController == null) throw new NotSupportedException("ParentingViewController parent could not be found. Please file a bug."); EventHandler appearing = null, disappearing = null; appearing = (s, e) => { parentViewController.Appearing -= appearing; parentViewController.Disappearing -= disappearing; Device.BeginInvokeOnMainThread(() => { tcs.SetResult(true); }); }; disappearing = (s, e) => { parentViewController.Appearing -= appearing; parentViewController.Disappearing -= disappearing; Device.BeginInvokeOnMainThread(() => { tcs.SetResult(false); }); }; parentViewController.Appearing += appearing; parentViewController.Disappearing += disappearing; return tcs.Task; } void HandlePropertyChanged(object sender, PropertyChangedEventArgs e) { #pragma warning disable 0618 //retaining legacy call to obsolete code if (e.PropertyName == NavigationPage.TintProperty.PropertyName) #pragma warning restore 0618 { UpdateTint(); } else if (e.PropertyName == NavigationPage.BarBackgroundColorProperty.PropertyName) { UpdateBarBackgroundColor(); } else if (e.PropertyName == NavigationPage.BarTextColorProperty.PropertyName || e.PropertyName == StatusBarTextColorModeProperty.PropertyName) { UpdateBarTextColor(); SetStatusBarStyle(); } else if (e.PropertyName == VisualElement.BackgroundColorProperty.PropertyName) { UpdateBackgroundColor(); } else if (e.PropertyName == NavigationPage.CurrentPageProperty.PropertyName) { Current = NavPage?.CurrentPage; ValidateNavbarExists(Current); } else if (e.PropertyName == IsNavigationBarTranslucentProperty.PropertyName) { UpdateTranslucent(); } else if (e.PropertyName == PreferredStatusBarUpdateAnimationProperty.PropertyName) { UpdateCurrentPagePreferredStatusBarUpdateAnimation(); } else if (e.PropertyName == PrefersLargeTitlesProperty.PropertyName) { UpdateUseLargeTitles(); } else if (e.PropertyName == NavigationPage.BackButtonTitleProperty.PropertyName) { var pack = (ParentingViewController)TopViewController; pack?.UpdateTitleArea(pack.Child); } else if (e.PropertyName == HideNavigationBarSeparatorProperty.PropertyName) { UpdateHideNavigationBarSeparator(); } } void ValidateNavbarExists(Page newCurrentPage) { //if the last time we did ViewDidLayoutSubviews we had other value for _hasNavigationBar //we will need to relayout. This is because Current is updated async of the layout happening if (_hasNavigationBar != NavigationPage.GetHasNavigationBar(newCurrentPage)) ViewDidLayoutSubviews(); } void UpdateHideNavigationBarSeparator() { bool shouldHide = NavPage.OnThisPlatform().HideNavigationBarSeparator(); // Just setting the ShadowImage is good for iOS11 if (_defaultNavBarShadowImage == null) _defaultNavBarShadowImage = NavigationBar.ShadowImage; if (shouldHide) NavigationBar.ShadowImage = new UIImage(); else NavigationBar.ShadowImage = _defaultNavBarShadowImage; if (!Forms.IsiOS11OrNewer) { // For iOS 10 and lower, you need to set the background image. // If you set this for iOS11, you'll remove the background color. if (_defaultNavBarBackImage == null) _defaultNavBarBackImage = NavigationBar.GetBackgroundImage(UIBarMetrics.Default); if (shouldHide) NavigationBar.SetBackgroundImage(new UIImage(), UIBarMetrics.Default); else NavigationBar.SetBackgroundImage(_defaultNavBarBackImage, UIBarMetrics.Default); } } void UpdateCurrentPagePreferredStatusBarUpdateAnimation() { // Not using the extension method syntax here because for some reason it confuses the mono compiler // and throws a CS0121 error PageUIStatusBarAnimation animation = PlatformConfiguration.iOSSpecific.Page.PreferredStatusBarUpdateAnimation(((Page)Element).OnThisPlatform()); PlatformConfiguration.iOSSpecific.Page.SetPreferredStatusBarUpdateAnimation(Current.OnThisPlatform(), animation); } void UpdateUseLargeTitles() { if (Forms.IsiOS11OrNewer && NavPage != null) NavigationBar.PrefersLargeTitles = NavPage.OnThisPlatform().PrefersLargeTitles(); } void UpdateTranslucent() { NavigationBar.Translucent = NavPage.OnThisPlatform().IsNavigationBarTranslucent(); } void InsertPageBefore(Page page, Page before) { if (before == null) throw new ArgumentNullException("before"); if (page == null) throw new ArgumentNullException("page"); var pageContainer = CreateViewControllerForPage(page); var target = Platform.GetRenderer(before).ViewController.ParentViewController; ViewControllers = ViewControllers.Insert(ViewControllers.IndexOf(target), pageContainer); } void OnInsertPageBeforeRequested(object sender, NavigationRequestedEventArgs e) { InsertPageBefore(e.Page, e.BeforePage); } void OnPopRequested(object sender, NavigationRequestedEventArgs e) { e.Task = PopViewAsync(e.Page, e.Animated); } void OnPopToRootRequested(object sender, NavigationRequestedEventArgs e) { e.Task = PopToRootAsync(e.Page, e.Animated); } void OnPushRequested(object sender, NavigationRequestedEventArgs e) { // If any text entry controls have focus, we need to end their editing session // so that they are not the first responder; if we don't some things (like the activity indicator // on pull-to-refresh) will not work correctly. View?.Window?.EndEditing(true); e.Task = PushPageAsync(e.Page, e.Animated); } void OnRemovedPageRequested(object sender, NavigationRequestedEventArgs e) { RemovePage(e.Page); } void RemovePage(Page page) { if (page == null) throw new ArgumentNullException("page"); if (page == Current) throw new NotSupportedException(); // should never happen as NavPage protects against this var target = Platform.GetRenderer(page).ViewController.ParentViewController; // So the ViewControllers property is not very property like on iOS. Assigning to it doesn't cause it to be // immediately reflected into the property. The change will not be reflected until there has been sufficient time // to process it (it ends up on the event queue). So to resolve this issue we keep our own stack until we // know iOS has processed it, and make sure any updates use that. // In the future we may want to make RemovePageAsync and deprecate RemovePage to handle cases where Push/Pop is called // during a remove cycle. if (_removeControllers == null) { _removeControllers = ViewControllers.Remove(target); ViewControllers = _removeControllers; Device.BeginInvokeOnMainThread(() => { _removeControllers = null; }); } else { _removeControllers = _removeControllers.Remove(target); ViewControllers = _removeControllers; } var parentingViewController = ViewControllers.Last() as ParentingViewController; parentingViewController?.UpdateLeftBarButtonItem(page); } void RemoveViewControllers(bool animated) { var controller = TopViewController as ParentingViewController; if (controller == null || controller.Child == null || Platform.GetRenderer(controller.Child) == null) return; // Gesture in progress, lets not be proactive and just wait for it to finish var task = GetAppearedOrDisappearedTask(controller.Child); task.ContinueWith(t => { // task returns true if the user lets go of the page and is not popped // however at this point the renderer is already off the visual stack so we just need to update the NavigationPage // Also worth noting this task returns on the main thread if (t.Result) return; // because we skip the normal pop process we need to dispose ourselves controller?.Dispose(); }, TaskScheduler.FromCurrentSynchronizationContext()); } void UpdateBackgroundColor() { var color = Element.BackgroundColor == Color.Default ? Color.White : Element.BackgroundColor; View.BackgroundColor = color.ToUIColor(); } void UpdateBarBackgroundColor() { var barBackgroundColor = NavPage.BarBackgroundColor; // Set navigation bar background color NavigationBar.BarTintColor = barBackgroundColor == Color.Default ? UINavigationBar.Appearance.BarTintColor : barBackgroundColor.ToUIColor(); } void UpdateBarTextColor() { var barTextColor = NavPage.BarTextColor; var globalAttributes = UINavigationBar.Appearance.GetTitleTextAttributes(); if (barTextColor == Color.Default) { if (NavigationBar.TitleTextAttributes != null) { var attributes = new UIStringAttributes(); attributes.ForegroundColor = globalAttributes.TextColor; attributes.Font = globalAttributes.Font; NavigationBar.TitleTextAttributes = attributes; } } else { var titleAttributes = new UIStringAttributes(); titleAttributes.Font = globalAttributes.Font; // TODO: the ternary if statement here will always return false because of the encapsulating if statement. // What was the intention? titleAttributes.ForegroundColor = barTextColor == Color.Default ? titleAttributes.ForegroundColor ?? UINavigationBar.Appearance.TintColor : barTextColor.ToUIColor(); NavigationBar.TitleTextAttributes = titleAttributes; } if (Forms.IsiOS11OrNewer) { var globalLargeTitleAttributes = UINavigationBar.Appearance.LargeTitleTextAttributes; if (globalLargeTitleAttributes == null) NavigationBar.LargeTitleTextAttributes = NavigationBar.TitleTextAttributes; } var statusBarColorMode = NavPage.OnThisPlatform().GetStatusBarTextColorMode(); // set Tint color (i. e. Back Button arrow and Text) NavigationBar.TintColor = barTextColor == Color.Default || statusBarColorMode == StatusBarTextColorMode.DoNotAdjust ? UINavigationBar.Appearance.TintColor : barTextColor.ToUIColor(); } void SetStatusBarStyle() { var barTextColor = NavPage.BarTextColor; var statusBarColorMode = NavPage.OnThisPlatform().GetStatusBarTextColorMode(); if (statusBarColorMode == StatusBarTextColorMode.DoNotAdjust || barTextColor.Luminosity <= 0.5) { // Use dark text color for status bar UIApplication.SharedApplication.StatusBarStyle = UIStatusBarStyle.Default; } else { // Use light text color for status bar UIApplication.SharedApplication.StatusBarStyle = UIStatusBarStyle.LightContent; } } void UpdateTint() { #pragma warning disable 0618 //retaining legacy call to obsolete code var tintColor = NavPage.Tint; #pragma warning restore 0618 NavigationBar.BarTintColor = tintColor == Color.Default ? UINavigationBar.Appearance.BarTintColor : tintColor.ToUIColor(); if (tintColor == Color.Default) NavigationBar.TintColor = UINavigationBar.Appearance.TintColor; else NavigationBar.TintColor = tintColor.Luminosity > 0.5 ? UIColor.Black : UIColor.White; } void UpdateToolBarVisible() { if (_secondaryToolbar == null) return; if (TopViewController != null && TopViewController.ToolbarItems != null && TopViewController.ToolbarItems.Any()) { _secondaryToolbar.Hidden = false; _secondaryToolbar.Items = TopViewController.ToolbarItems; } else { _secondaryToolbar.Hidden = true; //secondaryToolbar.Items = null; } TopViewController?.NavigationItem?.TitleView?.SizeToFit(); TopViewController?.NavigationItem?.TitleView?.LayoutSubviews(); } internal async Task UpdateFormsInnerNavigation(Page pageBeingRemoved) { if (NavPage == null) return; _ignorePopCall = true; if (Element.Navigation.NavigationStack.Contains(pageBeingRemoved)) await (NavPage as INavigationPageController)?.RemoveAsyncInner(pageBeingRemoved, false, true); _ignorePopCall = false; } internal static async void SetMasterLeftBarButton(UIViewController containerController, MasterDetailPage masterDetailPage) { if (!masterDetailPage.ShouldShowToolbarButton()) { containerController.NavigationItem.LeftBarButtonItem = null; return; } EventHandler handler = (o, e) => masterDetailPage.IsPresented = !masterDetailPage.IsPresented; bool shouldUseIcon = masterDetailPage.Master.Icon != null; if (shouldUseIcon) { try { var source = Internals.Registrar.Registered.GetHandlerForObject(masterDetailPage.Master.Icon); var icon = await source.LoadImageAsync(masterDetailPage.Master.Icon); containerController.NavigationItem.LeftBarButtonItem = new UIBarButtonItem(icon, UIBarButtonItemStyle.Plain, handler); } catch (Exception) { // Throws Exception otherwise would catch more specific exception type shouldUseIcon = false; } } if (!shouldUseIcon) { containerController.NavigationItem.LeftBarButtonItem = new UIBarButtonItem(masterDetailPage.Master.Title, UIBarButtonItemStyle.Plain, handler); } } internal void ValidateInsets() { nfloat navBottom = NavigationBar.Frame.Bottom; if (_navigationBottom != navBottom && Current != null) ViewDidLayoutSubviews(); } class SecondaryToolbar : UIToolbar { readonly List _lines = new List(); public SecondaryToolbar() { TintColor = UIColor.White; } public override UIBarButtonItem[] Items { get { return base.Items; } set { base.Items = value; SetupLines(); } } public override void LayoutSubviews() { base.LayoutSubviews(); if (Items == null || Items.Length == 0) return; LayoutToolbarItems(Bounds.Width, Bounds.Height, 0); } void LayoutToolbarItems(nfloat toolbarWidth, nfloat toolbarHeight, nfloat padding) { var x = padding; var y = 0; var itemH = toolbarHeight; var itemW = toolbarWidth / Items.Length; foreach (var item in Items) { var frame = new RectangleF(x, y, itemW, itemH); if (frame == item.CustomView.Frame) continue; item.CustomView.Frame = frame; x += itemW + padding; } x = itemW + padding * 1.5f; y = (int)Bounds.GetMidY(); foreach (var l in _lines) { l.Center = new PointF(x, y); x += itemW + padding; } } void SetupLines() { _lines.ForEach(l => l.RemoveFromSuperview()); _lines.Clear(); if (Items == null) return; for (var i = 1; i < Items.Length; i++) { var l = new UIView(new RectangleF(0, 0, 1, 24)) { BackgroundColor = new UIColor(0, 0, 0, 0.2f) }; AddSubview(l); _lines.Add(l); } } } class ParentingViewController : UIViewController { readonly WeakReference _navigation; Page _child; ToolbarTracker _tracker = new ToolbarTracker(); public ParentingViewController(NavigationRenderer navigation) { AutomaticallyAdjustsScrollViewInsets = false; _navigation = new WeakReference(navigation); } public Page Child { get { return _child; } set { if (_child == value) return; if (_child != null) _child.PropertyChanged -= HandleChildPropertyChanged; _child = value; if (_child != null) _child.PropertyChanged += HandleChildPropertyChanged; UpdateHasBackButton(); UpdateLargeTitles(); } } public event EventHandler Appearing; public override void DidRotate(UIInterfaceOrientation fromInterfaceOrientation) { base.DidRotate(fromInterfaceOrientation); View.SetNeedsLayout(); } public event EventHandler Disappearing; public override void ViewDidAppear(bool animated) { base.ViewDidAppear(animated); Appearing?.Invoke(this, EventArgs.Empty); } public override void ViewDidDisappear(bool animated) { base.ViewDidDisappear(animated); Disappearing?.Invoke(this, EventArgs.Empty); } public override void ViewWillLayoutSubviews() { base.ViewWillLayoutSubviews(); NavigationRenderer n; if (_navigation.TryGetTarget(out n)) n.ValidateInsets(); } public override void ViewDidLayoutSubviews() { IVisualElementRenderer childRenderer; if (Child != null && (childRenderer = Platform.GetRenderer(Child)) != null) childRenderer.NativeView.Frame = Child.Bounds.ToRectangleF(); base.ViewDidLayoutSubviews(); } public override void ViewDidLoad() { base.ViewDidLoad(); _tracker.Target = Child; _tracker.AdditionalTargets = Child.GetParentPages(); _tracker.CollectionChanged += TrackerOnCollectionChanged; UpdateToolbarItems(); } public override void ViewWillAppear(bool animated) { UpdateNavigationBarVisibility(animated); NavigationRenderer n; var isTranslucent = false; if (_navigation.TryGetTarget(out n)) isTranslucent = n.NavigationBar.Translucent; EdgesForExtendedLayout = isTranslucent ? UIRectEdge.All : UIRectEdge.None; base.ViewWillAppear(animated); } protected override void Dispose(bool disposing) { if (disposing) { Child.SendDisappearing(); if (Child != null) { Child.PropertyChanged -= HandleChildPropertyChanged; Child = null; } _tracker.Target = null; _tracker.CollectionChanged -= TrackerOnCollectionChanged; _tracker = null; if (NavigationItem.RightBarButtonItems != null) { for (var i = 0; i < NavigationItem.RightBarButtonItems.Length; i++) NavigationItem.RightBarButtonItems[i].Dispose(); } if (ToolbarItems != null) { for (var i = 0; i < ToolbarItems.Length; i++) ToolbarItems[i].Dispose(); } } base.Dispose(disposing); } void HandleChildPropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == NavigationPage.HasNavigationBarProperty.PropertyName) UpdateNavigationBarVisibility(true); else if (e.PropertyName == Page.TitleProperty.PropertyName) NavigationItem.Title = Child.Title; else if (e.PropertyName == NavigationPage.HasBackButtonProperty.PropertyName) UpdateHasBackButton(); else if (e.PropertyName == PrefersStatusBarHiddenProperty.PropertyName) UpdatePrefersStatusBarHidden(); else if (e.PropertyName == LargeTitleDisplayProperty.PropertyName) UpdateLargeTitles(); else if (e.PropertyName == NavigationPage.TitleIconProperty.PropertyName || e.PropertyName == NavigationPage.TitleViewProperty.PropertyName) UpdateTitleArea(Child); } internal void UpdateLeftBarButtonItem(Page pageBeingRemoved = null) { NavigationRenderer n; if (!_navigation.TryGetTarget(out n)) return; var currentChild = this.Child; var firstPage = n.NavPage.Pages.FirstOrDefault(); if (n._parentMasterDetailPage == null) return; if (firstPage != pageBeingRemoved && currentChild != firstPage && NavigationPage.GetHasBackButton(currentChild)) { NavigationItem.LeftBarButtonItem = null; return; } SetMasterLeftBarButton(this, n._parentMasterDetailPage); } public bool NeedsTitleViewContainer(Page page) => NavigationPage.GetTitleIcon(page) != null || NavigationPage.GetTitleView(page) != null; internal void UpdateBackButtonTitle(Page page) => UpdateBackButtonTitle(page.Title, NavigationPage.GetBackButtonTitle(page)); internal void UpdateBackButtonTitle(string title, string backButtonTitle) { if (!string.IsNullOrWhiteSpace(title)) NavigationItem.Title = title; if (backButtonTitle != null) // adding a custom event handler to UIBarButtonItem for navigating back seems to be ignored. NavigationItem.BackBarButtonItem = new UIBarButtonItem { Title = backButtonTitle, Style = UIBarButtonItemStyle.Plain }; else NavigationItem.BackBarButtonItem = null; } internal void UpdateTitleArea(Page page) { if (page == null) return; FileImageSource titleIcon = NavigationPage.GetTitleIcon(page); View titleView = NavigationPage.GetTitleView(page); bool needContainer = titleView != null || titleIcon != null; string backButtonText = NavigationPage.GetBackButtonTitle(page); bool isBackButtonTextSet = page.IsSet(NavigationPage.BackButtonTitleProperty); // on iOS 10 if the user hasn't set the back button text // we set it to an empty string so it's consistent with iOS 11 if (!Forms.IsiOS11OrNewer && !isBackButtonTextSet) backButtonText = ""; // First page and we have a master detail to contend with UpdateLeftBarButtonItem(); UpdateBackButtonTitle(page.Title, backButtonText); //var hadTitleView = NavigationItem.TitleView != null; ClearTitleViewContainer(); if (needContainer) { NavigationRenderer n; if (!_navigation.TryGetTarget(out n)) return; Container titleViewContainer = new Container(titleView, n.NavigationBar); UpdateTitleImage(titleViewContainer, titleIcon); NavigationItem.TitleView = titleViewContainer; } } async void UpdateTitleImage(Container titleViewContainer, FileImageSource titleIcon) { if (titleViewContainer == null) return; if (string.IsNullOrWhiteSpace(titleIcon)) { titleViewContainer.Icon = null; } else { var source = Internals.Registrar.Registered.GetHandlerForObject(titleIcon); var image = await source.LoadImageAsync(titleIcon); try { titleViewContainer.Icon = new UIImageView(image) { }; } catch { //UIImage ctor throws on file not found if MonoTouch.ObjCRuntime.Class.ThrowOnInitFailure is true; } } } void ClearTitleViewContainer() { if (NavigationItem.TitleView != null && NavigationItem.TitleView is Container titleViewContainer) { titleViewContainer.Dispose(); titleViewContainer = null; NavigationItem.TitleView = null; } } void UpdatePrefersStatusBarHidden() { View.SetNeedsLayout(); ParentViewController?.View.SetNeedsLayout(); } void TrackerOnCollectionChanged(object sender, EventArgs eventArgs) { UpdateToolbarItems(); } void UpdateHasBackButton() { if (Child == null || NavigationItem.HidesBackButton == !NavigationPage.GetHasBackButton(Child)) return; NavigationItem.HidesBackButton = !NavigationPage.GetHasBackButton(Child); NavigationRenderer n; if (!_navigation.TryGetTarget(out n)) return; if (!Forms.IsiOS11OrNewer || n._parentMasterDetailPage != null) UpdateTitleArea(Child); } void UpdateNavigationBarVisibility(bool animated) { var current = Child; if (current == null || NavigationController == null) return; var hasNavBar = NavigationPage.GetHasNavigationBar(current); if (NavigationController.NavigationBarHidden == hasNavBar) { // prevent bottom content "jumping" current.IgnoresContainerArea = !hasNavBar; NavigationController.SetNavigationBarHidden(!hasNavBar, animated); } } void UpdateToolbarItems() { if (NavigationItem.RightBarButtonItems != null) { for (var i = 0; i < NavigationItem.RightBarButtonItems.Length; i++) NavigationItem.RightBarButtonItems[i].Dispose(); } if (ToolbarItems != null) { for (var i = 0; i < ToolbarItems.Length; i++) ToolbarItems[i].Dispose(); } List primaries = null; List secondaries = null; foreach (var item in _tracker.ToolbarItems) { if (item.Order == ToolbarItemOrder.Secondary) (secondaries = secondaries ?? new List()).Add(item.ToUIBarButtonItem(true)); else (primaries = primaries ?? new List()).Add(item.ToUIBarButtonItem()); } if (primaries != null) primaries.Reverse(); NavigationItem.SetRightBarButtonItems(primaries == null ? new UIBarButtonItem[0] : primaries.ToArray(), false); ToolbarItems = secondaries == null ? new UIBarButtonItem[0] : secondaries.ToArray(); NavigationRenderer n; if (_navigation.TryGetTarget(out n)) n.UpdateToolBarVisible(); } void UpdateLargeTitles() { var page = Child; if (page != null && Forms.IsiOS11OrNewer) { var largeTitleDisplayMode = page.OnThisPlatform().LargeTitleDisplay(); switch (largeTitleDisplayMode) { case LargeTitleDisplayMode.Always: NavigationItem.LargeTitleDisplayMode = UINavigationItemLargeTitleDisplayMode.Always; break; case LargeTitleDisplayMode.Automatic: NavigationItem.LargeTitleDisplayMode = UINavigationItemLargeTitleDisplayMode.Automatic; break; case LargeTitleDisplayMode.Never: NavigationItem.LargeTitleDisplayMode = UINavigationItemLargeTitleDisplayMode.Never; break; } } } public override UIInterfaceOrientationMask GetSupportedInterfaceOrientations() { IVisualElementRenderer childRenderer; if (Child != null && (childRenderer = Platform.GetRenderer(Child)) != null) return childRenderer.ViewController.GetSupportedInterfaceOrientations(); return base.GetSupportedInterfaceOrientations(); } public override UIInterfaceOrientation PreferredInterfaceOrientationForPresentation() { IVisualElementRenderer childRenderer; if (Child != null && (childRenderer = Platform.GetRenderer(Child)) != null) return childRenderer.ViewController.PreferredInterfaceOrientationForPresentation(); return base.PreferredInterfaceOrientationForPresentation(); } public override bool ShouldAutorotate() { IVisualElementRenderer childRenderer; if (Child != null && (childRenderer = Platform.GetRenderer(Child)) != null) return childRenderer.ViewController.ShouldAutorotate(); return base.ShouldAutorotate(); } public override bool ShouldAutorotateToInterfaceOrientation(UIInterfaceOrientation toInterfaceOrientation) { IVisualElementRenderer childRenderer; if (Child != null && (childRenderer = Platform.GetRenderer(Child)) != null) return childRenderer.ViewController.ShouldAutorotateToInterfaceOrientation(toInterfaceOrientation); return base.ShouldAutorotateToInterfaceOrientation(toInterfaceOrientation); } public override bool ShouldAutomaticallyForwardRotationMethods => true; public override async void DidMoveToParentViewController(UIViewController parent) { //we are being removed from the UINavigationPage if (parent == null) { NavigationRenderer navRenderer; if (_navigation.TryGetTarget(out navRenderer)) await navRenderer.UpdateFormsInnerNavigation(Child); } base.DidMoveToParentViewController(parent); } } public override UIViewController ChildViewControllerForStatusBarHidden() { return (UIViewController)Platform.GetRenderer(Current); } void IEffectControlProvider.RegisterEffect(Effect effect) { VisualElementRenderer.RegisterEffect(effect, View); } internal class FormsNavigationBar : UINavigationBar { public FormsNavigationBar() : base() { } public FormsNavigationBar(Foundation.NSCoder coder) : base(coder) { } protected FormsNavigationBar(Foundation.NSObjectFlag t) : base(t) { } protected internal FormsNavigationBar(IntPtr handle) : base(handle) { } public FormsNavigationBar(RectangleF frame) : base(frame) { } public RectangleF BackButtonFrameSize { get; private set; } public UILabel NavBarLabel { get; private set; } public override void LayoutSubviews() { if (!Forms.IsiOS11OrNewer) { for (int i = 0; i < this.Subviews.Length; i++) { if (Subviews[i] is UIView view) { if (view.Class.Name == "_UINavigationBarBackIndicatorView") { if (view.Alpha == 0) BackButtonFrameSize = CGRect.Empty; else BackButtonFrameSize = view.Frame; break; } else if(view.Class.Name == "UINavigationItemButtonView") { if (view.Subviews.Length == 0) NavBarLabel = null; else if (view.Subviews[0] is UILabel titleLabel) NavBarLabel = titleLabel; } } } } base.LayoutSubviews(); } } class Container : UIView { View _view; FormsNavigationBar _bar; IVisualElementRenderer _child; UIImageView _icon; public Container(View view, UINavigationBar bar) : base(bar.Bounds) { if (Forms.IsiOS11OrNewer) { TranslatesAutoresizingMaskIntoConstraints = false; } else { TranslatesAutoresizingMaskIntoConstraints = true; AutoresizingMask = UIViewAutoresizing.FlexibleHeight | UIViewAutoresizing.FlexibleWidth; } _bar = bar as FormsNavigationBar; if (view != null) { _view = view; _child = Platform.CreateRenderer(view); Platform.SetRenderer(view, _child); AddSubview(_child.NativeView); } ClipsToBounds = true; } public override CGSize IntrinsicContentSize => UILayoutFittingExpandedSize; nfloat IconHeight => _icon?.Frame.Height ?? 0; nfloat IconWidth => _icon?.Frame.Width ?? 0; // Navigation bar will not stretch past these values. Prevent content clipping. // iOS11 does this for us automatically, but apparently iOS10 doesn't. nfloat ToolbarHeight { get { if (Superview?.Bounds.Height > 0) return Superview.Bounds.Height; return (Device.Idiom == TargetIdiom.Phone && Device.Info.CurrentOrientation.IsLandscape()) ? 32 : 44; } } public override CGRect Frame { get => base.Frame; set { if (Superview != null) { if (!Forms.IsiOS11OrNewer) { value.Y = Superview.Bounds.Y; if (_bar != null && String.IsNullOrWhiteSpace(_bar.NavBarLabel?.Text) && _bar.BackButtonFrameSize != RectangleF.Empty) { var xSpace = _bar.BackButtonFrameSize.Width + (_bar.BackButtonFrameSize.X * 2); value.Width = (value.X - xSpace) + value.Width; value.X = xSpace; } }; value.Height = ToolbarHeight; } base.Frame = value; } } public UIImageView Icon { set { if (_icon != null) _icon.RemoveFromSuperview(); _icon = value; if (_icon != null) AddSubview(_icon); } } public override SizeF SizeThatFits(SizeF size) { return new SizeF(size.Width, ToolbarHeight); } public override void LayoutSubviews() { base.LayoutSubviews(); if (Frame == CGRect.Empty || Frame.Width >= 10000 || Frame.Height >= 10000) return; nfloat toolbarHeight = ToolbarHeight; double height = Math.Min(toolbarHeight, Bounds.Height); if (_icon != null) _icon.Frame = new RectangleF(0, 0, IconWidth, Math.Min(toolbarHeight, IconHeight)); if (_child?.Element != null) { var layoutBounds = new Rectangle(IconWidth, 0, Bounds.Width - IconWidth, height); if (_child.Element.Bounds != layoutBounds) Layout.LayoutChildIntoBoundingRegion(_child.Element, layoutBounds); } } protected override void Dispose(bool disposing) { if (disposing) { if (_child != null) { _child.Element?.DisposeModalAndChildRenderers(); _child.NativeView.RemoveFromSuperview(); _child.Dispose(); _child = null; } _view = null; _icon?.Dispose(); _icon = null; } base.Dispose(disposing); } } } }