From 8382e5331d2d9c1e7900cfb61a0a22dabd5646b3 Mon Sep 17 00:00:00 2001 From: Martijn van Dijk Date: Mon, 6 May 2019 21:38:58 +0200 Subject: [PATCH] Add attribute to show page on iOS (#3339) * Add attribute to show page on iOS * Fixed some small issues in iOS * Adjusted the implementation for TvOs * Added all the views and viewmodels for the ios sample * Updated the page views * Added all the views to the storyboard, added the right viewmodels and updated the presenters * Updated the MvxPageViewController, so we now hold a list of pages in there so we can show them correctly --- .../MvxPagePresentationAttribute.cs | 14 ++ .../Ios/Presenters/MvxIosViewPresenter.cs | 55 ++++++ .../Ios/Views/IMvxPageViewController.cs | 17 ++ .../Ios/Views/MvxBasePageViewController.cs | 168 ++++++++++++++++++ .../Ios/Views/MvxPageViewController.cs | 147 ++++----------- .../MvxPagePresentationAttribute.cs | 14 ++ .../Tvos/Presenters/MvxTvosViewPresenter.cs | 53 ++++++ .../Tvos/Views/IMvxPageViewController.cs | 17 ++ .../Tvos/Views/MvxBasePageViewController.cs | 168 ++++++++++++++++++ .../Tvos/Views/MvxPageViewController.cs | 149 ++++------------ MvvmCross/ViewModels/IMvxPageViewModel.cs | 20 --- .../ViewModels/Navigation/Page1ViewModel.cs | 20 +++ .../ViewModels/Navigation/Page2ViewModel.cs | 18 ++ .../ViewModels/Navigation/Page3ViewModel.cs | 19 ++ .../Navigation/PagesRootViewModel.cs | 33 ++++ .../ViewModels/RootViewModel.cs | 4 + .../Playground/Playground.iOS/Main.storyboard | 116 ++++++++++-- .../Playground.iOS/Playground.iOS.csproj | 16 ++ .../Playground.iOS/Views/Page1View.cs | 17 ++ .../Views/Page1View.designer.cs | 21 +++ .../Playground.iOS/Views/Page2View.cs | 17 ++ .../Views/Page2View.designer.cs | 21 +++ .../Playground.iOS/Views/Page3View.cs | 17 ++ .../Views/Page3View.designer.cs | 21 +++ .../Playground.iOS/Views/PagesRootView.cs | 31 ++++ .../Views/PagesRootView.designer.cs | 21 +++ .../Playground.iOS/Views/RootView.cs | 2 +- .../Playground.iOS/Views/RootView.designer.cs | 9 + 28 files changed, 959 insertions(+), 266 deletions(-) create mode 100644 MvvmCross/Platforms/Ios/Presenters/Attributes/MvxPagePresentationAttribute.cs create mode 100644 MvvmCross/Platforms/Ios/Views/IMvxPageViewController.cs create mode 100644 MvvmCross/Platforms/Ios/Views/MvxBasePageViewController.cs create mode 100644 MvvmCross/Platforms/Tvos/Presenters/Attributes/MvxPagePresentationAttribute.cs create mode 100644 MvvmCross/Platforms/Tvos/Views/IMvxPageViewController.cs create mode 100644 MvvmCross/Platforms/Tvos/Views/MvxBasePageViewController.cs delete mode 100644 MvvmCross/ViewModels/IMvxPageViewModel.cs create mode 100644 Projects/Playground/Playground.Core/ViewModels/Navigation/Page1ViewModel.cs create mode 100644 Projects/Playground/Playground.Core/ViewModels/Navigation/Page2ViewModel.cs create mode 100644 Projects/Playground/Playground.Core/ViewModels/Navigation/Page3ViewModel.cs create mode 100644 Projects/Playground/Playground.Core/ViewModels/Navigation/PagesRootViewModel.cs create mode 100644 Projects/Playground/Playground.iOS/Views/Page1View.cs create mode 100644 Projects/Playground/Playground.iOS/Views/Page1View.designer.cs create mode 100644 Projects/Playground/Playground.iOS/Views/Page2View.cs create mode 100644 Projects/Playground/Playground.iOS/Views/Page2View.designer.cs create mode 100644 Projects/Playground/Playground.iOS/Views/Page3View.cs create mode 100644 Projects/Playground/Playground.iOS/Views/Page3View.designer.cs create mode 100644 Projects/Playground/Playground.iOS/Views/PagesRootView.cs create mode 100644 Projects/Playground/Playground.iOS/Views/PagesRootView.designer.cs diff --git a/MvvmCross/Platforms/Ios/Presenters/Attributes/MvxPagePresentationAttribute.cs b/MvvmCross/Platforms/Ios/Presenters/Attributes/MvxPagePresentationAttribute.cs new file mode 100644 index 0000000000..5a9ef5e0fb --- /dev/null +++ b/MvvmCross/Platforms/Ios/Presenters/Attributes/MvxPagePresentationAttribute.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MS-PL license. +// See the LICENSE file in the project root for more information. + +using MvvmCross.Presenters.Attributes; + +namespace MvvmCross.Platforms.Ios.Presenters.Attributes +{ + public class MvxPagePresentationAttribute : MvxBasePresentationAttribute + { + public static bool DefaultWrapInNavigationController = false; + public bool WrapInNavigationController { get; set; } = DefaultWrapInNavigationController; + } +} diff --git a/MvvmCross/Platforms/Ios/Presenters/MvxIosViewPresenter.cs b/MvvmCross/Platforms/Ios/Presenters/MvxIosViewPresenter.cs index 0cd6e34789..a810949392 100644 --- a/MvvmCross/Platforms/Ios/Presenters/MvxIosViewPresenter.cs +++ b/MvvmCross/Platforms/Ios/Presenters/MvxIosViewPresenter.cs @@ -30,6 +30,8 @@ public class MvxIosViewPresenter : MvxAttributeViewPresenter, IMvxIosViewPresent public IMvxTabBarViewController TabBarViewController { get; protected set; } + public IMvxPageViewController PageViewController { get; protected set; } + public IMvxSplitViewController SplitViewController { get; protected set; } public override MvxBasePresentationAttribute CreatePresentationAttribute(Type viewModelType, Type viewType) @@ -109,6 +111,14 @@ public override void RegisterAttributeTypes() }, CloseTabViewController); + AttributeTypesToActionsDictionary.Register( + (viewType, attribute, request) => + { + var viewController = (UIViewController)this.CreateViewControllerFor(request); + return ShowPageViewController(viewController, attribute, request); + }, + ClosePageViewController); + AttributeTypesToActionsDictionary.Register( (viewType, attribute, request) => { @@ -166,6 +176,19 @@ public override void RegisterAttributeTypes() return true; } + if (viewController is IMvxPageViewController pageViewController) + { + PageViewController = pageViewController; + + // set root + SetupWindowRootNavigation(viewController, attribute); + + if (!await CloseModalViewControllers()) return false; + if (!await CloseSplitViewController()) return false; + + return true; + } + // check if viewController is a SplitViewController if (viewController is IMvxSplitViewController splitController) { @@ -300,6 +323,30 @@ protected void SetupWindowRootNavigation(UIViewController viewController, MvxRoo return Task.FromResult(true); } + protected virtual Task ShowPageViewController( + UIViewController viewController, + MvxPagePresentationAttribute attribute, + MvxViewModelRequest request) + { + if (PageViewController == null) + throw new MvxException("Trying to show a page without a PageViewController, this is not possible!"); + + /*if (viewController is IMvxTabBarItemViewController tabBarItem) + { + attribute.TabName = tabBarItem.TabName; + attribute.TabIconName = tabBarItem.TabIconName; + attribute.TabSelectedIconName = tabBarItem.TabSelectedIconName; + }*/ + + if (attribute.WrapInNavigationController) + viewController = CreateNavigationController(viewController); + + PageViewController.AddPage( + viewController, + attribute); + return Task.FromResult(true); + } + protected virtual Task ShowModalViewController( UIViewController viewController, MvxModalPresentationAttribute attribute, @@ -393,6 +440,14 @@ protected virtual Task CloseTabViewController(IMvxViewModel viewModel, Mvx return Task.FromResult(false); } + protected virtual Task ClosePageViewController(IMvxViewModel viewModel, MvxPagePresentationAttribute attribute) + { + if (PageViewController != null && PageViewController.RemovePage(viewModel)) + return Task.FromResult(true); + + return Task.FromResult(false); + } + protected virtual Task CloseMasterSplitViewController(IMvxViewModel viewModel, MvxSplitViewPresentationAttribute attribute) { if (SplitViewController != null && SplitViewController.CloseChildViewModel(viewModel, attribute)) diff --git a/MvvmCross/Platforms/Ios/Views/IMvxPageViewController.cs b/MvvmCross/Platforms/Ios/Views/IMvxPageViewController.cs new file mode 100644 index 0000000000..6a63386420 --- /dev/null +++ b/MvvmCross/Platforms/Ios/Views/IMvxPageViewController.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MS-PL license. +// See the LICENSE file in the project root for more information. + +using MvvmCross.Platforms.Ios.Presenters.Attributes; +using MvvmCross.ViewModels; +using UIKit; + +namespace MvvmCross.Platforms.Ios.Views +{ + public interface IMvxPageViewController + { + void AddPage(UIViewController viewController, MvxPagePresentationAttribute attribute); + + bool RemovePage (IMvxViewModel viewModel); + } +} diff --git a/MvvmCross/Platforms/Ios/Views/MvxBasePageViewController.cs b/MvvmCross/Platforms/Ios/Views/MvxBasePageViewController.cs new file mode 100644 index 0000000000..58b4af9982 --- /dev/null +++ b/MvvmCross/Platforms/Ios/Views/MvxBasePageViewController.cs @@ -0,0 +1,168 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MS-PL license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using Foundation; +using MvvmCross.Logging; +using MvvmCross.Binding.BindingContext; +using MvvmCross.Platforms.Ios.Views.Base; +using MvvmCross.ViewModels; +using UIKit; + +namespace MvvmCross.Platforms.Ios.Views +{ + public class MvxBasePageViewController : MvxEventSourcePageViewController, IMvxIosView + { + public MvxBasePageViewController(UIPageViewControllerTransitionStyle style = UIPageViewControllerTransitionStyle.Scroll, UIPageViewControllerNavigationOrientation navigationOrientation = UIPageViewControllerNavigationOrientation.Horizontal, UIPageViewControllerSpineLocation spineLocation = UIPageViewControllerSpineLocation.None) : base(style, navigationOrientation, spineLocation) + { + this.AdaptForBinding(); + } + + public MvxBasePageViewController(UIPageViewControllerTransitionStyle style, UIPageViewControllerNavigationOrientation navigationOrientation, UIPageViewControllerSpineLocation spineLocation, float interPageSpacing) : base(style, navigationOrientation, spineLocation, interPageSpacing) + { + this.AdaptForBinding(); + } + + public MvxBasePageViewController(UIPageViewControllerTransitionStyle style, UIPageViewControllerNavigationOrientation navigationOrientation) : base(style, navigationOrientation) + { + this.AdaptForBinding(); + } + + public MvxBasePageViewController(NSCoder coder) : base(coder) + { + this.AdaptForBinding(); + } + + protected MvxBasePageViewController(NSObjectFlag t) : base(t) + { + this.AdaptForBinding(); + } + + protected internal MvxBasePageViewController(IntPtr handle) : base(handle) + { + this.AdaptForBinding(); + } + + public MvxBasePageViewController(string nibName, NSBundle bundle) : base(nibName, bundle) + { + this.AdaptForBinding(); + } + + public MvxBasePageViewController(UIPageViewControllerTransitionStyle style, UIPageViewControllerNavigationOrientation navigationOrientation, NSDictionary options) : base(style, navigationOrientation, options) + { + this.AdaptForBinding(); + } + public object DataContext + { + get + { + // special code needed in PageViewController because View is initialized during construction + return BindingContext?.DataContext; + } + set + { + BindingContext.DataContext = value; + } + } + + public IMvxViewModel ViewModel + { + get { return DataContext as IMvxViewModel; } + set { DataContext = value; } + } + + public MvxViewModelRequest Request { get; set; } + + public IMvxBindingContext BindingContext { get; set; } + + public override void ViewDidLoad() + { + base.ViewDidLoad(); + ViewModel?.ViewCreated(); + } + + public override void ViewWillAppear(bool animated) + { + base.ViewWillAppear(animated); + ViewModel?.ViewAppearing(); + } + + public override void ViewDidAppear(bool animated) + { + base.ViewDidAppear(animated); + ViewModel?.ViewAppeared(); + } + + public override void ViewWillDisappear(bool animated) + { + base.ViewWillDisappear(animated); + ViewModel?.ViewDisappearing(); + } + + public override void ViewDidDisappear(bool animated) + { + base.ViewDidDisappear(animated); + ViewModel?.ViewDisappeared(); + } + + public override void DidMoveToParentViewController(UIViewController parent) + { + base.DidMoveToParentViewController(parent); + if (parent == null) + ViewModel?.ViewDestroy(); + } + + public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender) + { + base.PrepareForSegue(segue, sender); + this.ViewModelRequestForSegue(segue, sender); + } + } + + public class MvxBasePageViewController : MvxPageViewController, IMvxIosView where TViewModel : class, IMvxViewModel + { + public MvxBasePageViewController() + { + } + + public MvxBasePageViewController(NSCoder coder) : base(coder) + { + } + + public MvxBasePageViewController(UIPageViewControllerTransitionStyle style, UIPageViewControllerNavigationOrientation navigationOrientation) : base(style, navigationOrientation) + { + } + + public MvxBasePageViewController(string nibName, NSBundle bundle) : base(nibName, bundle) + { + } + + public MvxBasePageViewController(UIPageViewControllerTransitionStyle style, UIPageViewControllerNavigationOrientation navigationOrientation, UIPageViewControllerSpineLocation spineLocation) : base(style, navigationOrientation, spineLocation) + { + } + + public MvxBasePageViewController(UIPageViewControllerTransitionStyle style, UIPageViewControllerNavigationOrientation navigationOrientation, NSDictionary options) : base(style, navigationOrientation, options) + { + } + + public MvxBasePageViewController(UIPageViewControllerTransitionStyle style, UIPageViewControllerNavigationOrientation navigationOrientation, UIPageViewControllerSpineLocation spineLocation, float interPageSpacing) : base(style, navigationOrientation, spineLocation, interPageSpacing) + { + } + + protected MvxBasePageViewController(NSObjectFlag t) : base(t) + { + } + + protected internal MvxBasePageViewController(IntPtr handle) : base(handle) + { + } + + public new TViewModel ViewModel + { + get { return (TViewModel)base.ViewModel; } + set { base.ViewModel = value; } + } + } +} diff --git a/MvvmCross/Platforms/Ios/Views/MvxPageViewController.cs b/MvvmCross/Platforms/Ios/Views/MvxPageViewController.cs index a2df213b0c..f98e8acf9d 100644 --- a/MvvmCross/Platforms/Ios/Views/MvxPageViewController.cs +++ b/MvvmCross/Platforms/Ios/Views/MvxPageViewController.cs @@ -10,180 +10,95 @@ using MvvmCross.Platforms.Ios.Views.Base; using MvvmCross.ViewModels; using UIKit; +using MvvmCross.Platforms.Ios.Presenters.Attributes; +using System.Linq; namespace MvvmCross.Platforms.Ios.Views { - public class MvxPageViewController : MvxEventSourcePageViewController, IMvxIosView + public class MvxPageViewController : MvxBasePageViewController, IMvxPageViewController { public MvxPageViewController(UIPageViewControllerTransitionStyle style = UIPageViewControllerTransitionStyle.Scroll, UIPageViewControllerNavigationOrientation navigationOrientation = UIPageViewControllerNavigationOrientation.Horizontal, UIPageViewControllerSpineLocation spineLocation = UIPageViewControllerSpineLocation.None) : base(style, navigationOrientation, spineLocation) { - this.AdaptForBinding(); } public MvxPageViewController(UIPageViewControllerTransitionStyle style, UIPageViewControllerNavigationOrientation navigationOrientation, UIPageViewControllerSpineLocation spineLocation, float interPageSpacing) : base(style, navigationOrientation, spineLocation, interPageSpacing) { - this.AdaptForBinding(); } public MvxPageViewController(UIPageViewControllerTransitionStyle style, UIPageViewControllerNavigationOrientation navigationOrientation) : base(style, navigationOrientation) { - this.AdaptForBinding(); } public MvxPageViewController(NSCoder coder) : base(coder) { - this.AdaptForBinding(); } protected MvxPageViewController(NSObjectFlag t) : base(t) { - this.AdaptForBinding(); } protected internal MvxPageViewController(IntPtr handle) : base(handle) { - this.AdaptForBinding(); } public MvxPageViewController(string nibName, NSBundle bundle) : base(nibName, bundle) { - this.AdaptForBinding(); } public MvxPageViewController(UIPageViewControllerTransitionStyle style, UIPageViewControllerNavigationOrientation navigationOrientation, NSDictionary options) : base(style, navigationOrientation, options) { - this.AdaptForBinding(); } - private Dictionary _pagedViewControllerCache = new Dictionary(); - - public MvxViewModelRequest Request { get; set; } - public IMvxBindingContext BindingContext { get; set; } - - public override void ViewWillAppear(bool animated) + public override void ViewDidLoad() { - base.ViewWillAppear(animated); - ViewModel?.ViewAppearing(); + base.ViewDidLoad(); + + GetNextViewController = (pc, rc) => GetNextViewControllerPage(rc); + GetPreviousViewController = (pc, rc) => GetPreviousViewControllerPage(rc); } - public override void ViewDidAppear(bool animated) - { - base.ViewDidAppear(animated); - ViewModel?.ViewAppeared(); - } + private List Pages = new List(); - public override void ViewWillDisappear(bool animated) - { - base.ViewWillDisappear(animated); - ViewModel?.ViewDisappearing(); - } + public bool IsFirstPage(UIViewController viewController) => Pages.IndexOf(viewController) == 0; - public override void ViewDidDisappear(bool animated) - { - base.ViewDidDisappear(animated); - ViewModel?.ViewDisappeared(); - } + public bool IsLastPage(UIViewController viewController) => Pages.IndexOf(viewController) == Pages.Count - 1; - public override void DidMoveToParentViewController(UIViewController parent) - { - base.DidMoveToParentViewController(parent); - if (parent == null) - ViewModel?.ViewDestroy(); - } + protected UIViewController GetNextViewControllerPage(UIViewController rc) => IsLastPage(rc) ? null : Pages[Pages.IndexOf(rc) + 1]; - public IMvxViewModel ViewModel - { - get - { - return DataContext as IMvxViewModel; - } - set - { - DataContext = value; - //Verify ViewModel is IMvxPageViewModel - if (DataContext != null && !(DataContext is IMvxPageViewModel)) - MvxLog.Instance.Error("Error - MvxPageViewController must be given an instance of IMvxPageViewModel"); - } - } + protected UIViewController GetPreviousViewControllerPage(UIViewController rc) => IsFirstPage(rc) ? null : Pages[Pages.IndexOf(rc) - 1]; - public object DataContext + public void AddPage(UIViewController viewController, MvxPagePresentationAttribute attribute) { - get { return BindingContext.DataContext; } - set { BindingContext.DataContext = value; } - } + // add Page + Pages.Add(viewController); - protected virtual void InitializePaging() - { - IMvxPageViewModel pageVM = ViewModel as IMvxPageViewModel; - if (pageVM == null) - return; - IMvxPagedViewModel defaultVM = pageVM.GetDefaultViewModel(); - UIViewController defaultVC = GetViewControllerForViewModel(defaultVM); - SetViewControllers(new UIViewController[] { defaultVC }, UIPageViewControllerNavigationDirection.Forward, true, null); - GetNextViewController = delegate (UIPageViewController pc, UIViewController rc) - { - IMvxIosView rcTV = rc as IMvxIosView; - if (rcTV == null) - return null; - IMvxPagedViewModel currentVM = rcTV.ViewModel as IMvxPagedViewModel; - if (currentVM == null) - return null; - IMvxPagedViewModel nextVM = pageVM.GetNextViewModel(currentVM); - if (nextVM == null) - return null; - UIViewController nextVC = GetViewControllerForViewModel(nextVM); - return nextVC; - }; - GetPreviousViewController = delegate (UIPageViewController pc, UIViewController rc) + // Start the ui page view controller when we add the first page + if (Pages.Count == 1) { - IMvxIosView rcTV = rc as IMvxIosView; - if (rcTV == null) - return null; - IMvxPagedViewModel currentVM = rcTV.ViewModel as IMvxPagedViewModel; - if (currentVM == null) - return null; - IMvxPagedViewModel prevVM = pageVM.GetPreviousViewModel(currentVM); - if (prevVM == null) - return null; - UIViewController prevVC = GetViewControllerForViewModel(prevVM); - return prevVC; - }; + SetViewControllers(Pages.ToArray(), UIPageViewControllerNavigationDirection.Forward, true, null); + } } - public override void ViewDidLoad() + public bool RemovePage(IMvxViewModel viewModel) { - base.ViewDidLoad(); - ViewModel?.ViewCreated(); - InitializePaging(); - } + if (Pages == null || !Pages.Any()) + return false; - public virtual void NavigateToViewModel(IMvxPagedViewModel targetVM, UIPageViewControllerNavigationDirection direction, bool animated = true) - { - UIViewController targetVC = GetViewControllerForViewModel(targetVM); - SetViewControllers(new UIViewController[] { targetVC }, direction, animated, null); - } + var pageToClose = Pages.Where(v => !(v is UINavigationController)) + .Select(v => v.GetIMvxIosView()) + .FirstOrDefault(mvxView => mvxView.ViewModel == viewModel); - public virtual UIViewController GetViewControllerForViewModel(IMvxPagedViewModel queryVM) - { - UIViewController retVal = null; - if (_pagedViewControllerCache.ContainsKey(queryVM.PagedViewId)) - retVal = _pagedViewControllerCache[queryVM.PagedViewId]; - else + if (pageToClose != null) { - retVal = this.CreateViewControllerFor(queryVM) as UIViewController; - _pagedViewControllerCache[queryVM.PagedViewId] = retVal; + Pages = Pages.Where(v => v != pageToClose).ToList(); + return true; } - return retVal; - } - public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender) - { - base.PrepareForSegue(segue, sender); - this.ViewModelRequestForSegue(segue, sender); + return false; } } - public class MvxPageViewController : MvxPageViewController, IMvxIosView where TViewModel : class, IMvxPageViewModel + public class MvxPageViewController : MvxPageViewController, IMvxIosView where TViewModel : class, IMvxViewModel { public MvxPageViewController() { diff --git a/MvvmCross/Platforms/Tvos/Presenters/Attributes/MvxPagePresentationAttribute.cs b/MvvmCross/Platforms/Tvos/Presenters/Attributes/MvxPagePresentationAttribute.cs new file mode 100644 index 0000000000..b6c02954a8 --- /dev/null +++ b/MvvmCross/Platforms/Tvos/Presenters/Attributes/MvxPagePresentationAttribute.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MS-PL license. +// See the LICENSE file in the project root for more information. + +using MvvmCross.Presenters.Attributes; + +namespace MvvmCross.Platforms.Tvos.Presenters.Attributes +{ + public class MvxPagePresentationAttribute : MvxBasePresentationAttribute + { + public static bool DefaultWrapInNavigationController = false; + public bool WrapInNavigationController { get; set; } = DefaultWrapInNavigationController; + } +} diff --git a/MvvmCross/Platforms/Tvos/Presenters/MvxTvosViewPresenter.cs b/MvvmCross/Platforms/Tvos/Presenters/MvxTvosViewPresenter.cs index 181c2c5622..9783d6b113 100644 --- a/MvvmCross/Platforms/Tvos/Presenters/MvxTvosViewPresenter.cs +++ b/MvvmCross/Platforms/Tvos/Presenters/MvxTvosViewPresenter.cs @@ -33,6 +33,8 @@ public class MvxTvosViewPresenter public IMvxTabBarViewController TabBarViewController { get; protected set; } + public IMvxPageViewController PageViewController { get; protected set; } + public MvxSplitViewController SplitViewController { get; protected set; } public MvxTvosViewPresenter(IUIApplicationDelegate applicationDelegate, UIWindow window) @@ -119,6 +121,14 @@ public override void RegisterAttributeTypes() }, CloseTabViewController); + AttributeTypesToActionsDictionary.Register( + (viewType, attribute, request) => + { + var viewController = (UIViewController)this.CreateViewControllerFor(request); + return ShowPageViewController(viewController, attribute, request); + }, + ClosePageViewController); + AttributeTypesToActionsDictionary.Register( (viewType, attribute, request) => { @@ -188,6 +198,14 @@ public Task CloseTabBarViewController() return Task.FromResult(false); } + protected virtual Task ClosePageViewController(IMvxViewModel viewModel, MvxPagePresentationAttribute attribute) + { + if (PageViewController != null && PageViewController.RemovePage(viewModel)) + return Task.FromResult(true); + + return Task.FromResult(false); + } + protected virtual Task CloseModalViewController(IMvxViewModel viewModel, MvxModalPresentationAttribute attribute) { @@ -326,6 +344,17 @@ protected Task CloseSplitViewController() return await CloseModalViewControllers(); } + if (viewController is IMvxPageViewController) + { + //NOTE clean up must be done first incase we are enbedding into a navigation controller + //before setting the page view controller, otherwise this will reset the view stack and your page + //controller will be null. + await SetupWindowRootNavigation(viewController, attribute); + this.PageViewController = (IMvxPageViewController)viewController; + + return await CloseModalViewControllers(); + } + await SetupWindowRootNavigation(viewController, attribute); if(!(await CloseModalViewControllers()))return false; @@ -418,6 +447,30 @@ protected Task CloseSplitViewController() return Task.FromResult(true); } + protected virtual Task ShowPageViewController( + UIViewController viewController, + MvxPagePresentationAttribute attribute, + MvxViewModelRequest request) + { + if (PageViewController == null) + throw new MvxException("Trying to show a page without a PageViewController, this is not possible!"); + + /*if (viewController is IMvxTabBarItemViewController tabBarItem) + { + attribute.TabName = tabBarItem.TabName; + attribute.TabIconName = tabBarItem.TabIconName; + attribute.TabSelectedIconName = tabBarItem.TabSelectedIconName; + }*/ + + if (attribute.WrapInNavigationController) + viewController = CreateNavigationController(viewController); + + PageViewController.AddPage( + viewController, + attribute); + return Task.FromResult(true); + } + protected virtual async Task ShowMasterDetailSplitViewController( UIViewController viewController, MvxMasterDetailPresentationAttribute attribute, diff --git a/MvvmCross/Platforms/Tvos/Views/IMvxPageViewController.cs b/MvvmCross/Platforms/Tvos/Views/IMvxPageViewController.cs new file mode 100644 index 0000000000..f6727af34b --- /dev/null +++ b/MvvmCross/Platforms/Tvos/Views/IMvxPageViewController.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MS-PL license. +// See the LICENSE file in the project root for more information. + +using MvvmCross.Platforms.Tvos.Presenters.Attributes; +using MvvmCross.ViewModels; +using UIKit; + +namespace MvvmCross.Platforms.Tvos.Views +{ + public interface IMvxPageViewController + { + void AddPage(UIViewController viewController, MvxPagePresentationAttribute attribute); + + bool RemovePage (IMvxViewModel viewModel); + } +} diff --git a/MvvmCross/Platforms/Tvos/Views/MvxBasePageViewController.cs b/MvvmCross/Platforms/Tvos/Views/MvxBasePageViewController.cs new file mode 100644 index 0000000000..3d82b3e7e3 --- /dev/null +++ b/MvvmCross/Platforms/Tvos/Views/MvxBasePageViewController.cs @@ -0,0 +1,168 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MS-PL license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using Foundation; +using MvvmCross.Logging; +using MvvmCross.Binding.BindingContext; +using MvvmCross.Platforms.Tvos.Views.Base; +using MvvmCross.ViewModels; +using UIKit; + +namespace MvvmCross.Platforms.Tvos.Views +{ + public class MvxBasePageViewController : MvxEventSourcePageViewController, IMvxTvosView + { + public MvxBasePageViewController(UIPageViewControllerTransitionStyle style = UIPageViewControllerTransitionStyle.Scroll, UIPageViewControllerNavigationOrientation navigationOrientation = UIPageViewControllerNavigationOrientation.Horizontal, UIPageViewControllerSpineLocation spineLocation = UIPageViewControllerSpineLocation.None) : base(style, navigationOrientation, spineLocation) + { + this.AdaptForBinding(); + } + + public MvxBasePageViewController(UIPageViewControllerTransitionStyle style, UIPageViewControllerNavigationOrientation navigationOrientation, UIPageViewControllerSpineLocation spineLocation, float interPageSpacing) : base(style, navigationOrientation, spineLocation, interPageSpacing) + { + this.AdaptForBinding(); + } + + public MvxBasePageViewController(UIPageViewControllerTransitionStyle style, UIPageViewControllerNavigationOrientation navigationOrientation) : base(style, navigationOrientation) + { + this.AdaptForBinding(); + } + + public MvxBasePageViewController(NSCoder coder) : base(coder) + { + this.AdaptForBinding(); + } + + protected MvxBasePageViewController(NSObjectFlag t) : base(t) + { + this.AdaptForBinding(); + } + + protected internal MvxBasePageViewController(IntPtr handle) : base(handle) + { + this.AdaptForBinding(); + } + + public MvxBasePageViewController(string nibName, NSBundle bundle) : base(nibName, bundle) + { + this.AdaptForBinding(); + } + + public MvxBasePageViewController(UIPageViewControllerTransitionStyle style, UIPageViewControllerNavigationOrientation navigationOrientation, NSDictionary options) : base(style, navigationOrientation, options) + { + this.AdaptForBinding(); + } + public object DataContext + { + get + { + // special code needed in PageViewController because View is initialized during construction + return BindingContext?.DataContext; + } + set + { + BindingContext.DataContext = value; + } + } + + public IMvxViewModel ViewModel + { + get { return DataContext as IMvxViewModel; } + set { DataContext = value; } + } + + public MvxViewModelRequest Request { get; set; } + + public IMvxBindingContext BindingContext { get; set; } + + public override void ViewDidLoad() + { + base.ViewDidLoad(); + ViewModel?.ViewCreated(); + } + + public override void ViewWillAppear(bool animated) + { + base.ViewWillAppear(animated); + ViewModel?.ViewAppearing(); + } + + public override void ViewDidAppear(bool animated) + { + base.ViewDidAppear(animated); + ViewModel?.ViewAppeared(); + } + + public override void ViewWillDisappear(bool animated) + { + base.ViewWillDisappear(animated); + ViewModel?.ViewDisappearing(); + } + + public override void ViewDidDisappear(bool animated) + { + base.ViewDidDisappear(animated); + ViewModel?.ViewDisappeared(); + } + + public override void DidMoveToParentViewController(UIViewController parent) + { + base.DidMoveToParentViewController(parent); + if (parent == null) + ViewModel?.ViewDestroy(); + } + + public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender) + { + base.PrepareForSegue(segue, sender); + this.ViewModelRequestForSegue(segue, sender); + } + } + + public class MvxBasePageViewController : MvxPageViewController, IMvxTvosView where TViewModel : class, IMvxViewModel + { + public MvxBasePageViewController() + { + } + + public MvxBasePageViewController(NSCoder coder) : base(coder) + { + } + + public MvxBasePageViewController(UIPageViewControllerTransitionStyle style, UIPageViewControllerNavigationOrientation navigationOrientation) : base(style, navigationOrientation) + { + } + + public MvxBasePageViewController(string nibName, NSBundle bundle) : base(nibName, bundle) + { + } + + public MvxBasePageViewController(UIPageViewControllerTransitionStyle style, UIPageViewControllerNavigationOrientation navigationOrientation, UIPageViewControllerSpineLocation spineLocation) : base(style, navigationOrientation, spineLocation) + { + } + + public MvxBasePageViewController(UIPageViewControllerTransitionStyle style, UIPageViewControllerNavigationOrientation navigationOrientation, NSDictionary options) : base(style, navigationOrientation, options) + { + } + + public MvxBasePageViewController(UIPageViewControllerTransitionStyle style, UIPageViewControllerNavigationOrientation navigationOrientation, UIPageViewControllerSpineLocation spineLocation, float interPageSpacing) : base(style, navigationOrientation, spineLocation, interPageSpacing) + { + } + + protected MvxBasePageViewController(NSObjectFlag t) : base(t) + { + } + + protected internal MvxBasePageViewController(IntPtr handle) : base(handle) + { + } + + public new TViewModel ViewModel + { + get { return (TViewModel)base.ViewModel; } + set { base.ViewModel = value; } + } + } +} diff --git a/MvvmCross/Platforms/Tvos/Views/MvxPageViewController.cs b/MvvmCross/Platforms/Tvos/Views/MvxPageViewController.cs index 17ff609cfd..259c23fcc6 100644 --- a/MvvmCross/Platforms/Tvos/Views/MvxPageViewController.cs +++ b/MvvmCross/Platforms/Tvos/Views/MvxPageViewController.cs @@ -4,186 +4,99 @@ using System; using System.Collections.Generic; +using System.Linq; using Foundation; -using MvvmCross.Logging; using MvvmCross.Binding.BindingContext; -using MvvmCross.Platforms.Tvos.Views.Base; +using MvvmCross.Platforms.Tvos.Presenters.Attributes; using MvvmCross.ViewModels; using UIKit; namespace MvvmCross.Platforms.Tvos.Views { - public class MvxPageViewController : MvxEventSourcePageViewController, IMvxTvosView + public class MvxPageViewController : MvxBasePageViewController, IMvxPageViewController { - private Dictionary _pagedViewControllerCache = new Dictionary(); - public MvxPageViewController(UIPageViewControllerTransitionStyle style = UIPageViewControllerTransitionStyle.Scroll, UIPageViewControllerNavigationOrientation navigationOrientation = UIPageViewControllerNavigationOrientation.Horizontal, UIPageViewControllerSpineLocation spineLocation = UIPageViewControllerSpineLocation.None) : base(style, navigationOrientation, spineLocation) { - this.AdaptForBinding(); } public MvxPageViewController(UIPageViewControllerTransitionStyle style, UIPageViewControllerNavigationOrientation navigationOrientation, UIPageViewControllerSpineLocation spineLocation, float interPageSpacing) : base(style, navigationOrientation, spineLocation, interPageSpacing) { - this.AdaptForBinding(); } public MvxPageViewController(UIPageViewControllerTransitionStyle style, UIPageViewControllerNavigationOrientation navigationOrientation) : base(style, navigationOrientation) { - this.AdaptForBinding(); } public MvxPageViewController(NSCoder coder) : base(coder) { - this.AdaptForBinding(); } protected MvxPageViewController(NSObjectFlag t) : base(t) { - this.AdaptForBinding(); } protected internal MvxPageViewController(IntPtr handle) : base(handle) { - this.AdaptForBinding(); } public MvxPageViewController(string nibName, NSBundle bundle) : base(nibName, bundle) { - this.AdaptForBinding(); } public MvxPageViewController(UIPageViewControllerTransitionStyle style, UIPageViewControllerNavigationOrientation navigationOrientation, NSDictionary options) : base(style, navigationOrientation, options) { - this.AdaptForBinding(); } - public MvxViewModelRequest Request { get; set; } - public IMvxBindingContext BindingContext { get; set; } - - public override void ViewWillAppear(bool animated) + public override void ViewDidLoad() { - base.ViewWillAppear(animated); - ViewModel?.ViewAppearing(); - } + base.ViewDidLoad(); - public override void ViewDidAppear(bool animated) - { - base.ViewDidAppear(animated); - ViewModel?.ViewAppeared(); + GetNextViewController = (pc, rc) => GetNextViewControllerPage(rc); + GetPreviousViewController = (pc, rc) => GetPreviousViewControllerPage(rc); } - public override void ViewWillDisappear(bool animated) - { - base.ViewWillDisappear(animated); - ViewModel?.ViewDisappearing(); - } + private List Pages = new List(); - public override void ViewDidDisappear(bool animated) - { - base.ViewDidDisappear(animated); - ViewModel?.ViewDisappeared(); - } + public bool IsFirstPage(UIViewController viewController) => Pages.IndexOf(viewController) == 0; - public override void DidMoveToParentViewController(UIViewController parent) - { - base.DidMoveToParentViewController(parent); - if(parent == null) - ViewModel?.ViewDestroy(); - } + public bool IsLastPage(UIViewController viewController) => Pages.IndexOf(viewController) == Pages.Count - 1; - public IMvxViewModel ViewModel - { - get - { - return DataContext as IMvxViewModel; - } - set - { - DataContext = value; - //Verify ViewModel is IMvxPageViewModel - if(DataContext != null && !(DataContext is IMvxPageViewModel)) - MvxLog.Instance.Error("Error - MvxPageViewController must be given an instance of IMvxPageViewModel"); - } - } + protected UIViewController GetNextViewControllerPage(UIViewController rc) => IsLastPage(rc) ? null : Pages[Pages.IndexOf(rc) + 1]; - public object DataContext - { - get { return BindingContext.DataContext; } - set { BindingContext.DataContext = value; } - } + protected UIViewController GetPreviousViewControllerPage(UIViewController rc) => IsFirstPage(rc) ? null : Pages[Pages.IndexOf(rc) - 1]; - protected virtual void InitializePaging() + public void AddPage(UIViewController viewController, MvxPagePresentationAttribute attribute) { - IMvxPageViewModel pageVM = ViewModel as IMvxPageViewModel; - if(pageVM == null) - return; - IMvxPagedViewModel defaultVM = pageVM.GetDefaultViewModel(); - UIViewController defaultVC = GetViewControllerForViewModel(defaultVM); - SetViewControllers(new UIViewController[] { defaultVC }, UIPageViewControllerNavigationDirection.Forward, true, null); - GetNextViewController = delegate (UIPageViewController pc, UIViewController rc) - { - IMvxTvosView rcTV = rc as IMvxTvosView; - if(rcTV == null) - return null; - IMvxPagedViewModel currentVM = rcTV.ViewModel as IMvxPagedViewModel; - if(currentVM == null) - return null; - IMvxPagedViewModel nextVM = pageVM.GetNextViewModel(currentVM); - if(nextVM == null) - return null; - UIViewController nextVC = GetViewControllerForViewModel(nextVM); - return nextVC; - }; - GetPreviousViewController = delegate (UIPageViewController pc, UIViewController rc) + // add Page + Pages.Add(viewController); + + // Start the ui page view controller when we add the first page + if (Pages.Count == 1) { - IMvxTvosView rcTV = rc as IMvxTvosView; - if(rcTV == null) - return null; - IMvxPagedViewModel currentVM = rcTV.ViewModel as IMvxPagedViewModel; - if(currentVM == null) - return null; - IMvxPagedViewModel prevVM = pageVM.GetPreviousViewModel(currentVM); - if(prevVM == null) - return null; - UIViewController prevVC = GetViewControllerForViewModel(prevVM); - return prevVC; - }; + SetViewControllers(Pages.ToArray(), UIPageViewControllerNavigationDirection.Forward, true, null); + } } - public override void ViewDidLoad() + public bool RemovePage(IMvxViewModel viewModel) { - base.ViewDidLoad(); - ViewModel?.ViewCreated(); - InitializePaging(); - } + if (Pages == null || !Pages.Any()) + return false; - public virtual void NavigateToViewModel(IMvxPagedViewModel targetVM, UIPageViewControllerNavigationDirection direction, bool animated = true) - { - UIViewController targetVC = GetViewControllerForViewModel(targetVM); - SetViewControllers(new UIViewController[] { targetVC }, direction, animated, null); - } + var pageToClose = Pages.Where(v => !(v is UINavigationController)) + .Select(v => v.GetIMvxTvosView()) + .FirstOrDefault(mvxView => mvxView.ViewModel == viewModel); - public virtual UIViewController GetViewControllerForViewModel(IMvxPagedViewModel queryVM) - { - UIViewController retVal = null; - if(_pagedViewControllerCache.ContainsKey(queryVM.PagedViewId)) - retVal = _pagedViewControllerCache[queryVM.PagedViewId]; - else + if (pageToClose != null) { - retVal = this.CreateViewControllerFor(queryVM) as UIViewController; - _pagedViewControllerCache[queryVM.PagedViewId] = retVal; + Pages = Pages.Where(v => v != pageToClose).ToList(); + return true; } - return retVal; - } - public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender) - { - base.PrepareForSegue(segue, sender); - this.ViewModelRequestForSegue(segue, sender); + return false; } } - public class MvxPageViewController : MvxPageViewController, IMvxTvosView where TViewModel : class, IMvxPageViewModel + public class MvxPageViewController : MvxPageViewController, IMvxTvosView where TViewModel : class, IMvxViewModel { public MvxPageViewController(UIPageViewControllerTransitionStyle style = UIPageViewControllerTransitionStyle.Scroll, UIPageViewControllerNavigationOrientation orientation = UIPageViewControllerNavigationOrientation.Horizontal, UIPageViewControllerSpineLocation spine = UIPageViewControllerSpineLocation.None) : base(style, orientation, spine) { diff --git a/MvvmCross/ViewModels/IMvxPageViewModel.cs b/MvvmCross/ViewModels/IMvxPageViewModel.cs deleted file mode 100644 index b942a0aea5..0000000000 --- a/MvvmCross/ViewModels/IMvxPageViewModel.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MS-PL license. -// See the LICENSE file in the project root for more information. - -namespace MvvmCross.ViewModels -{ - public interface IMvxPagedViewModel : IMvxViewModel - { - string PagedViewId { get; } - } - - public interface IMvxPageViewModel : IMvxViewModel - { - IMvxPagedViewModel GetDefaultViewModel(); - - IMvxPagedViewModel GetNextViewModel(IMvxPagedViewModel currentViewModel); - - IMvxPagedViewModel GetPreviousViewModel(IMvxPagedViewModel currentViewModel); - } -} \ No newline at end of file diff --git a/Projects/Playground/Playground.Core/ViewModels/Navigation/Page1ViewModel.cs b/Projects/Playground/Playground.Core/ViewModels/Navigation/Page1ViewModel.cs new file mode 100644 index 0000000000..bc837cfab6 --- /dev/null +++ b/Projects/Playground/Playground.Core/ViewModels/Navigation/Page1ViewModel.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MS-PL license. +// See the LICENSE file in the project root for more information. + +using System.Threading.Tasks; +using MvvmCross.Commands; +using MvvmCross.Logging; +using MvvmCross.Navigation; +using MvvmCross.Presenters.Hints; +using MvvmCross.ViewModels; + +namespace Playground.Core.ViewModels +{ + public class Page1ViewModel : MvxNavigationViewModel + { + public Page1ViewModel(IMvxLogProvider logProvider, IMvxNavigationService navigationService) : base(logProvider, navigationService) + { + } + } +} diff --git a/Projects/Playground/Playground.Core/ViewModels/Navigation/Page2ViewModel.cs b/Projects/Playground/Playground.Core/ViewModels/Navigation/Page2ViewModel.cs new file mode 100644 index 0000000000..c2941af771 --- /dev/null +++ b/Projects/Playground/Playground.Core/ViewModels/Navigation/Page2ViewModel.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MS-PL license. +// See the LICENSE file in the project root for more information. + +using MvvmCross.Commands; +using MvvmCross.Logging; +using MvvmCross.Navigation; +using MvvmCross.ViewModels; + +namespace Playground.Core.ViewModels +{ + public class Page2ViewModel : MvxNavigationViewModel + { + public Page2ViewModel(IMvxLogProvider logProvider, IMvxNavigationService navigationService) : base(logProvider, navigationService) + { + } + } +} diff --git a/Projects/Playground/Playground.Core/ViewModels/Navigation/Page3ViewModel.cs b/Projects/Playground/Playground.Core/ViewModels/Navigation/Page3ViewModel.cs new file mode 100644 index 0000000000..20d67b9511 --- /dev/null +++ b/Projects/Playground/Playground.Core/ViewModels/Navigation/Page3ViewModel.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MS-PL license. +// See the LICENSE file in the project root for more information. + +using MvvmCross.Commands; +using MvvmCross.Logging; +using MvvmCross.Navigation; +using MvvmCross.Presenters.Hints; +using MvvmCross.ViewModels; + +namespace Playground.Core.ViewModels +{ + public class Page3ViewModel : MvxNavigationViewModel + { + public Page3ViewModel(IMvxLogProvider logProvider, IMvxNavigationService navigationService) : base(logProvider, navigationService) + { + } + } +} diff --git a/Projects/Playground/Playground.Core/ViewModels/Navigation/PagesRootViewModel.cs b/Projects/Playground/Playground.Core/ViewModels/Navigation/PagesRootViewModel.cs new file mode 100644 index 0000000000..0b7b45a105 --- /dev/null +++ b/Projects/Playground/Playground.Core/ViewModels/Navigation/PagesRootViewModel.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MS-PL license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MvvmCross.Commands; +using MvvmCross.Logging; +using MvvmCross.Navigation; +using MvvmCross.ViewModels; + +namespace Playground.Core.ViewModels +{ + public class PagesRootViewModel : MvxNavigationViewModel + { + public PagesRootViewModel(IMvxLogProvider logProvider, IMvxNavigationService navigationService) : base(logProvider, navigationService) + { + ShowInitialViewModelsCommand = new MvxAsyncCommand(ShowInitialViewModels); + } + + public IMvxAsyncCommand ShowInitialViewModelsCommand { get; private set; } + + private async Task ShowInitialViewModels() + { + var tasks = new List(); + tasks.Add(NavigationService.Navigate()); + tasks.Add(NavigationService.Navigate()); + tasks.Add(NavigationService.Navigate()); + await Task.WhenAll(tasks); + } + } +} diff --git a/Projects/Playground/Playground.Core/ViewModels/RootViewModel.cs b/Projects/Playground/Playground.Core/ViewModels/RootViewModel.cs index 99657f85df..eac1918bcd 100644 --- a/Projects/Playground/Playground.Core/ViewModels/RootViewModel.cs +++ b/Projects/Playground/Playground.Core/ViewModels/RootViewModel.cs @@ -65,6 +65,8 @@ public RootViewModel(IMvxLogProvider logProvider, IMvxNavigationService navigati ShowTabsCommand = new MvxAsyncCommand(async () => await NavigationService.Navigate()); + ShowPagesCommand = new MvxAsyncCommand(async () => await NavigationService.Navigate()); + ShowSplitCommand = new MvxAsyncCommand(async () => await NavigationService.Navigate()); ShowNativeCommand = new MvxAsyncCommand(async () => await NavigationService.Navigate()); @@ -117,6 +119,8 @@ public RootViewModel(IMvxLogProvider logProvider, IMvxNavigationService navigati public IMvxAsyncCommand ShowTabsCommand { get; } + public IMvxAsyncCommand ShowPagesCommand { get; } + public IMvxAsyncCommand ShowSplitCommand { get; } public IMvxAsyncCommand ShowOverrideAttributeCommand { get; } diff --git a/Projects/Playground/Playground.iOS/Main.storyboard b/Projects/Playground/Playground.iOS/Main.storyboard index 50f257474f..74311da105 100644 --- a/Projects/Playground/Playground.iOS/Main.storyboard +++ b/Projects/Playground/Playground.iOS/Main.storyboard @@ -18,19 +18,19 @@ - + - -