|
#nullable disable
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using Foundation;
using Microsoft.Maui.Controls.Handlers.Compatibility;
using Microsoft.Maui.Controls.Internals;
using ObjCRuntime;
using UIKit;
namespace Microsoft.Maui.Controls.Platform.Compatibility
{
public class ShellSectionRenderer : UINavigationController, IShellSectionRenderer, IAppearanceObserver, IDisconnectable
{
#region IShellContentRenderer
public bool IsInMoreTab { get; set; }
public ShellSection ShellSection
{
get { return _shellSection; }
set
{
if (_shellSection == value)
return;
_shellSection = value;
LoadPages();
OnShellSectionSet();
_shellSection.PropertyChanged += HandlePropertyChanged;
((IShellSectionController)_shellSection).NavigationRequested += OnNavigationRequested;
}
}
IShellSectionController ShellSectionController => ShellSection;
public UIViewController ViewController => this;
#endregion IShellContentRenderer
#region IAppearanceObserver
void IAppearanceObserver.OnAppearanceChanged(ShellAppearance appearance)
{
if (appearance == null)
_appearanceTracker.ResetAppearance(this);
else
_appearanceTracker.SetAppearance(this, appearance);
}
#endregion IAppearanceObserver
IShellContext _context;
readonly Dictionary<Element, IShellPageRendererTracker> _trackers =
new Dictionary<Element, IShellPageRendererTracker>();
IShellNavBarAppearanceTracker _appearanceTracker;
Dictionary<UIViewController, TaskCompletionSource<bool>> _completionTasks =
new Dictionary<UIViewController, TaskCompletionSource<bool>>();
Page _displayedPage;
bool _disposed;
bool _firstLayoutCompleted;
TaskCompletionSource<bool> _popCompletionTask;
IShellSectionRootRenderer _renderer;
ShellSection _shellSection;
bool _ignorePopCall;
// When setting base.ViewControllers iOS doesn't modify the property right away.
// if you set base.ViewControllers to a new array and then retrieve base.ViewControllers
// iOS will return the previous array until the new array has been processed
// This means if you try to remove one VC and then try to remove a second VC before the first one is processed
// you'll end up re-adding back the first VC
// ViewControllers = ViewControllers.Remove(vc1)
// ViewControllers = ViewControllers.Remove(vc2)
// You've now added vc1 back because the second call to ViewControllers will still return a ViewControllers list with vc1 in it
UIViewController[] _pendingViewControllers;
public ShellSectionRenderer(IShellContext context) : base()
{
Delegate = new NavDelegate(this);
_context = context;
_context.Shell.PropertyChanged += HandleShellPropertyChanged;
_context.Shell.Navigated += OnNavigated;
_context.Shell.Navigating += OnNavigating;
}
public ShellSectionRenderer(IShellContext context, Type navigationBarType, Type toolbarType)
: base(navigationBarType, toolbarType)
{
Delegate = new NavDelegate(this);
_context = context;
_context.Shell.PropertyChanged += HandleShellPropertyChanged;
_context.Shell.Navigated += OnNavigated;
_context.Shell.Navigating += OnNavigating;
}
[Export("navigationBar:shouldPopItem:")]
[Internals.Preserve(Conditional = true)]
public bool ShouldPopItem(UINavigationBar _, UINavigationItem __) =>
SendPop();
internal bool SendPop()
{
// this means the pop is already done, nothing we can do
if (ActiveViewControllers().Length < NavigationBar.Items.Length)
return true;
foreach (var tracker in _trackers)
{
if (tracker.Value.ViewController == TopViewController)
{
var behavior = Shell.GetBackButtonBehavior(tracker.Value.Page);
var command = behavior.GetPropertyIfSet<ICommand>(BackButtonBehavior.CommandProperty, null);
var commandParameter = behavior.GetPropertyIfSet<object>(BackButtonBehavior.CommandParameterProperty, null);
if (command != null)
{
if (command.CanExecute(commandParameter))
{
command.Execute(commandParameter);
}
return false;
}
break;
}
}
// Do not remove, wonky behavior on some versions of iOS if you dont dispatch
// Shane: ^ not sure if this is true anymore because of how
// we now route this through "GoToAsync"
CoreFoundation.DispatchQueue.MainQueue.DispatchAsync(async () =>
{
var navItemsCount = NavigationBar.Items.Length;
await _context.Shell.GoToAsync("..", true);
// This means the navigation was cancelled
if (NavigationBar.Items.Length == navItemsCount)
{
for (int i = 0; i < NavigationBar.Subviews.Length; i++)
{
var child = NavigationBar.Subviews[i];
if (child.Alpha != 1)
UIView.Animate(.2f, () => child.Alpha = 1);
}
}
});
return false;
}
public override void ViewDidDisappear(bool animated)
{
// If this page is removed from the View Hierarchy we need to resolve any
// pending navigation operations
var sourcesToComplete = new List<TaskCompletionSource<bool>>();
foreach (var item in _completionTasks.Values)
{
sourcesToComplete.Add(item);
}
_completionTasks.Clear();
foreach (var source in sourcesToComplete)
source.TrySetResult(false);
_popCompletionTask?.TrySetResult(false);
_popCompletionTask = null;
base.ViewDidDisappear(animated);
}
public override void ViewWillAppear(bool animated)
{
if (_disposed)
return;
UpdateFlowDirection();
base.ViewWillAppear(animated);
}
internal void UpdateFlowDirection()
{
View.UpdateFlowDirection(_context.Shell);
NavigationBar.UpdateFlowDirection(_context.Shell);
}
public override void ViewDidLayoutSubviews()
{
if (_disposed)
return;
base.ViewDidLayoutSubviews();
_appearanceTracker.UpdateLayout(this);
if (!_firstLayoutCompleted)
{
UpdateShadowImages();
_firstLayoutCompleted = true;
}
}
public override void ViewDidLoad()
{
if (_disposed)
return;
base.ViewDidLoad();
InteractivePopGestureRecognizer.Delegate = new GestureDelegate(this, ShouldPop);
UpdateFlowDirection();
}
public override void ViewDidAppear(bool animated)
{
base.ViewDidAppear(animated);
if (_context is ShellRenderer shellRenderer)
{
shellRenderer.ViewController.SetNeedsUpdateOfHomeIndicatorAutoHidden();
shellRenderer.ViewController.SetNeedsStatusBarAppearanceUpdate();
}
}
void IDisconnectable.Disconnect()
{
(_renderer as IDisconnectable)?.Disconnect();
if (_displayedPage != null)
_displayedPage.PropertyChanged -= OnDisplayedPagePropertyChanged;
if (_shellSection != null)
{
_shellSection.PropertyChanged -= HandlePropertyChanged;
((IShellSectionController)ShellSection).NavigationRequested -= OnNavigationRequested;
((IShellSectionController)ShellSection).RemoveDisplayedPageObserver(this);
}
if (_context.Shell != null)
{
_context.Shell.PropertyChanged -= HandleShellPropertyChanged;
_context.Shell.Navigated -= OnNavigated;
_context.Shell.Navigating -= OnNavigating;
((IShellController)_context.Shell).RemoveAppearanceObserver(this);
}
}
protected override void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
this.RemoveFromParentViewController();
_disposed = true;
_renderer.Dispose();
_appearanceTracker.Dispose();
(this as IDisconnectable).Disconnect();
foreach (var tracker in ShellSection.Stack)
{
if (tracker == null)
continue;
DisposePage(tracker, true);
}
}
_disposed = true;
_displayedPage = null;
_shellSection = null;
_appearanceTracker = null;
_renderer = null;
_context = null;
base.Dispose(disposing);
}
protected virtual void HandleShellPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.Is(VisualElement.FlowDirectionProperty))
UpdateFlowDirection();
}
protected virtual void HandlePropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == BaseShellItem.TitleProperty.PropertyName)
UpdateTabBarItem();
else if (e.PropertyName == BaseShellItem.IconProperty.PropertyName)
UpdateTabBarItem();
}
protected virtual IShellSectionRootRenderer CreateShellSectionRootRenderer(ShellSection shellSection, IShellContext shellContext)
{
return new ShellSectionRootRenderer(shellSection, shellContext);
}
protected virtual void LoadPages()
{
_renderer = CreateShellSectionRootRenderer(ShellSection, _context);
PushViewController(_renderer.ViewController, false);
var stack = ShellSection.Stack;
for (int i = 1; i < stack.Count; i++)
{
PushPage(stack[i], false);
}
}
protected virtual void OnDisplayedPageChanged(Page page)
{
if (_displayedPage == page)
return;
if (_displayedPage != null)
{
_displayedPage.PropertyChanged -= OnDisplayedPagePropertyChanged;
}
_displayedPage = page;
if (_displayedPage != null)
{
_displayedPage.PropertyChanged += OnDisplayedPagePropertyChanged;
UpdateNavigationBarHidden();
UpdateNavigationBarHasShadow();
}
}
protected virtual void OnInsertRequested(NavigationRequestedEventArgs e)
{
var page = e.Page;
var before = e.BeforePage;
var beforeRenderer = (IPlatformViewHandler)before.Handler;
var renderer = (IPlatformViewHandler)page.ToHandler(_shellSection.FindMauiContext());
var tracker = _context.CreatePageRendererTracker();
tracker.ViewController = renderer.ViewController;
tracker.Page = page;
_trackers[page] = tracker;
InsertViewController(ActiveViewControllers().IndexOf(beforeRenderer.ViewController), renderer.ViewController);
}
protected virtual void OnNavigationRequested(object sender, NavigationRequestedEventArgs e)
{
switch (e.RequestType)
{
case NavigationRequestType.Push:
OnPushRequested(e);
break;
case NavigationRequestType.Pop:
OnPopRequested(e);
break;
case NavigationRequestType.PopToRoot:
OnPopToRootRequested(e);
break;
case NavigationRequestType.Insert:
OnInsertRequested(e);
break;
case NavigationRequestType.Remove:
OnRemoveRequested(e);
break;
}
}
protected virtual async void OnPopRequested(NavigationRequestedEventArgs e)
{
var page = e.Page;
var animated = e.Animated;
_popCompletionTask = new TaskCompletionSource<bool>();
e.Task = _popCompletionTask.Task;
PopViewController(animated);
await _popCompletionTask.Task;
DisposePage(page);
}
public override UIViewController[] PopToRootViewController(bool animated)
{
if (!_ignorePopCall && ActiveViewControllers().Length > 1)
{
ProcessPopToRoot();
}
return base.PopToRootViewController(animated);
}
async void ProcessPopToRoot()
{
var task = new TaskCompletionSource<bool>();
var pages = _shellSection.Stack.ToList();
_completionTasks[_renderer.ViewController] = task;
((IShellSectionController)ShellSection).SendPoppingToRoot(task.Task);
await task.Task;
for (int i = pages.Count - 1; i >= 1; i--)
{
var page = pages[i];
DisposePage(page);
}
}
protected virtual async void OnPopToRootRequested(NavigationRequestedEventArgs e)
{
var animated = e.Animated;
var task = new TaskCompletionSource<bool>();
var pages = _shellSection.Stack.ToList();
try
{
_ignorePopCall = true;
_completionTasks[_renderer.ViewController] = task;
e.Task = task.Task;
PopToRootViewController(animated);
}
finally
{
_ignorePopCall = false;
}
await e.Task;
for (int i = pages.Count - 1; i >= 1; i--)
{
var page = pages[i];
DisposePage(page);
}
}
protected virtual void OnPushRequested(NavigationRequestedEventArgs e)
{
var page = e.Page;
var animated = e.Animated;
var taskSource = new TaskCompletionSource<bool>();
PushPage(page, animated, taskSource);
e.Task = taskSource.Task;
}
protected virtual void OnRemoveRequested(NavigationRequestedEventArgs e)
{
var page = e.Page;
var renderer = (IPlatformViewHandler)page.Handler;
var viewController = renderer?.ViewController;
if (viewController == null && _trackers.ContainsKey(page))
viewController = _trackers[page].ViewController;
if (viewController != null)
{
if (viewController == TopViewController)
{
e.Animated = false;
OnPopRequested(e);
}
RemoveViewController(viewController);
DisposePage(page);
}
}
protected virtual void OnShellSectionSet()
{
_appearanceTracker = _context.CreateNavBarAppearanceTracker();
UpdateTabBarItem();
((IShellController)_context.Shell).AddAppearanceObserver(this, ShellSection);
((IShellSectionController)ShellSection).AddDisplayedPageObserver(this, OnDisplayedPageChanged);
}
protected virtual void UpdateTabBarItem()
{
Title = ShellSection.Title;
ShellSection.Icon.LoadImage(ShellSection.FindMauiContext(), icon =>
{
TabBarItem = new UITabBarItem(ShellSection.Title, icon?.Value, null);
TabBarItem.AccessibilityIdentifier = ShellSection.AutomationId ?? ShellSection.Title;
});
}
void DisposePage(Page page, bool calledFromDispose = false)
{
if (_trackers.TryGetValue(page, out var tracker))
{
if (!calledFromDispose && tracker.ViewController != null && ActiveViewControllers().Contains(tracker.ViewController))
{
System.Diagnostics.Debug.Write($"Disposing {_trackers[page].ViewController.GetHashCode()}");
RemoveViewController(_trackers[page].ViewController);
}
tracker.Dispose();
_trackers.Remove(page);
}
var renderer = page.Handler;
if (renderer != null)
{
renderer.DisconnectHandler();
}
}
Element ElementForViewController(UIViewController viewController)
{
if (_renderer.ViewController == viewController)
return ShellSection;
foreach (var child in ShellSection.Stack)
{
if (child == null)
continue;
var renderer = (IPlatformViewHandler)child.Handler;
if (viewController == renderer.ViewController)
return child;
}
return null;
}
void OnDisplayedPagePropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == Shell.NavBarIsVisibleProperty.PropertyName)
UpdateNavigationBarHidden();
else if (e.PropertyName == Shell.NavBarHasShadowProperty.PropertyName)
UpdateNavigationBarHasShadow();
}
// We only care about using pendingViewControllers when we are setting the ViewControllers array directly
// So, once navigation starts again (or ends) we can just clear the pendingViewControllers
void OnNavigating(object sender, ShellNavigatingEventArgs e)
{
_pendingViewControllers = null;
}
void OnNavigated(object sender, ShellNavigatedEventArgs e)
{
_pendingViewControllers = null;
}
// These are all just safety nets to ensure that _pendingViewControllers doesn't for some reason get out of sync
// and start causing issues. In theory we could just override ViewControllers here to make sure _pendingViewControllers
// stays in sync but I don't trust that `ViewControllers.set` is reliably called with every modification
public override UIViewController[] ViewControllers
{
get => base.ViewControllers;
set
{
if (_pendingViewControllers != null)
_pendingViewControllers = value;
base.ViewControllers = value;
}
}
public override UIViewController[] PopToViewController(UIViewController viewController, bool animated)
{
_pendingViewControllers = null;
return base.PopToViewController(viewController, animated);
}
public override void PushViewController(UIViewController viewController, bool animated)
{
_pendingViewControllers = null;
base.PushViewController(viewController, animated);
}
public override UIViewController PopViewController(bool animated)
{
_pendingViewControllers = null;
return base.PopViewController(animated);
}
UIViewController[] ActiveViewControllers() =>
_pendingViewControllers ?? base.ViewControllers;
void RemoveViewController(UIViewController viewController)
{
_pendingViewControllers = _pendingViewControllers ?? base.ViewControllers;
if (_pendingViewControllers.Contains(viewController))
_pendingViewControllers = _pendingViewControllers.Remove(viewController);
ViewControllers = _pendingViewControllers;
}
void InsertViewController(int index, UIViewController viewController)
{
_pendingViewControllers = _pendingViewControllers ?? base.ViewControllers;
_pendingViewControllers = _pendingViewControllers.Insert(index, viewController);
ViewControllers = _pendingViewControllers;
}
void PushPage(Page page, bool animated, TaskCompletionSource<bool> completionSource = null)
{
var renderer = (IPlatformViewHandler)page.ToHandler(_shellSection.FindMauiContext());
var tracker = _context.CreatePageRendererTracker();
var pageViewController = renderer.ViewController!;
tracker.ViewController = pageViewController;
tracker.Page = page;
_trackers[page] = tracker;
var parentTabBar = ParentViewController as UITabBarController;
var showsPresentation = parentTabBar == null || ReferenceEquals(parentTabBar.SelectedViewController, this);
if (completionSource != null && showsPresentation)
_completionTasks[pageViewController] = completionSource;
PushViewController(pageViewController, animated);
if (completionSource != null && !showsPresentation)
completionSource.TrySetResult(true);
}
async void SendPoppedOnCompletion(Task popTask)
{
if (popTask == null)
{
throw new ArgumentNullException(nameof(popTask));
}
var poppedPage = _shellSection.Stack[_shellSection.Stack.Count - 1];
// this is used to setup appearance changes based on the incoming page
((IShellSectionController)_shellSection).SendPopping(popTask);
await popTask;
DisposePage(poppedPage);
}
bool ShouldPop()
{
var shellItem = _context.Shell.CurrentItem;
var shellSection = shellItem?.CurrentItem;
var shellContent = shellSection?.CurrentItem;
var stack = shellSection?.Stack.ToList();
stack?.RemoveAt(stack.Count - 1);
return ((IShellController)_context.Shell).ProposeNavigation(ShellNavigationSource.Pop, shellItem, shellSection, shellContent, stack, true);
}
void UpdateNavigationBarHidden()
{
SetNavigationBarHidden(!Shell.GetNavBarIsVisible(_displayedPage), true);
}
void UpdateNavigationBarHasShadow()
{
_appearanceTracker.SetHasShadow(this, Shell.GetNavBarHasShadow(_displayedPage));
}
void UpdateShadowImages()
{
NavigationBar.SetValueForKey(NSObject.FromObject(true), new NSString("hidesShadow"));
}
class GestureDelegate : UIGestureRecognizerDelegate
{
readonly UINavigationController _parent;
readonly Func<bool> _shouldPop;
public GestureDelegate(UINavigationController parent, Func<bool> shouldPop)
{
_parent = parent;
_shouldPop = shouldPop;
}
public override bool ShouldBegin(UIGestureRecognizer recognizer)
{
if ((_parent as ShellSectionRenderer).ActiveViewControllers().Length == 1)
return false;
return _shouldPop();
}
}
class NavDelegate : UINavigationControllerDelegate
{
readonly ShellSectionRenderer _self;
public NavDelegate(ShellSectionRenderer renderer)
{
_self = renderer;
}
// This is currently working around a Mono Interpreter bug
// if you remove this code please verify that hot restart still works
// https://github.com/xamarin/Xamarin.Forms/issues/10519
[Export("navigationController:animationControllerForOperation:fromViewController:toViewController:")]
[Foundation.Preserve(Conditional = true)]
public new IUIViewControllerAnimatedTransitioning GetAnimationControllerForOperation(UINavigationController navigationController, UINavigationControllerOperation operation, UIViewController fromViewController, UIViewController toViewController)
{
return null;
}
public override void DidShowViewController(UINavigationController navigationController, [Transient] UIViewController viewController, bool animated)
{
var tasks = _self._completionTasks;
var popTask = _self._popCompletionTask;
if (tasks.TryGetValue(viewController, out var source))
{
source.TrySetResult(true);
tasks.Remove(viewController);
}
else if (popTask != null)
{
popTask.TrySetResult(true);
}
}
public override void WillShowViewController(UINavigationController navigationController, [Transient] UIViewController viewController, bool animated)
{
var element = _self.ElementForViewController(viewController);
bool navBarVisible = false;
if (element is not null)
{
if (element is ShellSection)
navBarVisible = _self._renderer.ShowNavBar;
else
navBarVisible = Shell.GetNavBarIsVisible(element);
}
navigationController.SetNavigationBarHidden(!navBarVisible, true);
var coordinator = viewController.GetTransitionCoordinator();
if (coordinator != null && coordinator.IsInteractive)
{
// handle swipe to dismiss gesture
coordinator.NotifyWhenInteractionChanges(OnInteractionChanged);
}
// Because the back button title needs to be set on the previous VC
// We want to set the BackButtonItem as early as possible so there is no flickering
var currentPage = _self._context?.Shell?.GetCurrentShellPage();
var trackers = _self._trackers;
if (currentPage?.Handler is IPlatformViewHandler pvh &&
pvh.ViewController == viewController &&
trackers.TryGetValue(currentPage, out var tracker) &&
tracker is ShellPageRendererTracker shellRendererTracker)
{
shellRendererTracker.UpdateToolbarItemsInternal(false);
}
}
void OnInteractionChanged(IUIViewControllerTransitionCoordinatorContext context)
{
if (!context.IsCancelled)
{
_self._popCompletionTask = new TaskCompletionSource<bool>();
_self.SendPoppedOnCompletion(_self._popCompletionTask.Task);
}
}
}
}
}
|