iOS 13 introduces new modal presentation styles and dismissal behaviour for non-fullscreen modals - a user can now swipe down to dismiss modals instead of needing a dedicated bar button that would traditionally call NavigationService.Close() on the VM (more info here).
When we attempt to close these modals via swiping, the view does close but the MvxNavigationService doesn't remove the closed view from the stack. When we then try to present another view, the system complains with a Warning: Attempt to present <MvvmCross_Platforms_Ios_Views_MvxNavigationController: 0x7fa961d32a00> on <MvvmCross_Platforms_Ios_Views_MvxNavigationController: 0x7fa963757400> whose view is not in the window hierarchy! output, and the navigation fails.
Swiping down to dismiss an iOS 13 non-fullscreen modal closes the ViewModel and allows new views to be presented.
MvxViewController with an MvxModalPresentationAttribute that has a ModalPresentationStyle that is not fullscreen (UIModalPresentationStyle.FormSheet for example).ModalInPresentation property on the MvxViewController is false, if set to true it will prevent dismissal by swiping down.MvxNavigationService to present your new view, then dismiss it by pulling down on the modal.Warning: Attempt to present <MvvmCross_Platforms_Ios_Views_MvxNavigationController: 0x7fa961d32a00> on <MvvmCross_Platforms_Ios_Views_MvxNavigationController: 0x7fa963757400> whose view is not in the window hierarchy!. If you look in the navigation stack, you will find that the ViewModel attached to the view you dismissed is still in the stack.There are two main workarounds right now:
ModalInPresentation to true. This will prevent the swipe down to dismiss with a bounce back animation while still using the shiny new presentation style.ModalPresentationStyle in the MvxModalPresentationAttribute to be UIModalPresentationStyle.Fullscreen. This will present the modal as it would have pre-iOS13.Version: 6.3.0
Platform:
There was a good write up about this on Medium. Importantly
Detecting Dismissal
As noted earlier, some apps might need to execute some code when a modally presented view controller is dismissed using a Cancel, Done or Save button (other than just dismissing it). For example, you might need to restart a timer in a game, or act upon some information that the user changed in the presented view controller. That code wonât be executed if the user dismisses with a swipe. Your button isnât pressed, so its action handler wonât be called. This could break the behaviour of your app.
The simplest way to avoid this problem is to prevent the interactive dismissal using isModalInPresentation. The user will have to tap a button to dismiss the view controller, just as they did before iOS 13. There is another wayâŠ
iOS 13 adds some new UIAdaptivePresentationControllerDelegate methods. These allow another object (typically the presenting view controller) to control whether the interactive dismissal should be allowed (an alternative to using isModalInPresentation), and to be informed when the interactive dismissal begins or completes. These methods are well-documented and clearly explained in WWDC 2019 224: Modernizing Your UI for IOS 13 starting at 15 minutes. Note that presentationControllerWillDismiss can be called multiple times if the user starts swiping to dismiss, changes their mind and then swipes again. The presentationControllerDidDismiss method is where you need to execute the extra code that currently occurs when a Cancel, Done or Save button is pressed (of course, you donât need to dismiss the presented view controller). These methods wonât be called if the view controller is dismissed programmatically. Therefore you will still need to execute your code in the button handler (or your own delegate) that triggers the dismissal, even when running on ios 13.
The presentationControllerDidAttemptToDismiss delegate method is interesting. It will be called if the user tries to swipe to dismiss but isModalInPresentation resulted in the dismissal being blocked. The WWDC video suggests showing an action sheet (which will be a popover on iPad) asking if the user wants to abandon or save their changes. This seems like a really good idea if the presented view controller has Cancel and Save/Done buttons: creating a new note, editing the properties for an object etc.
For a nested view controller on a navigation stack with Cancel and Save buttons (for example, the Timer Profiles screen inside Pommieâs Settings screen), I think itâs more complicated. The code for performing the save is probably in the view controller one level higher in the stack (the delegate of the top view controller), and not in the object that would be the UIAdaptivePresentationControllerDelegate. Trying to route the userâs choice to the object that can perform the save might be pretty messy. In my own apps, I think I will just block the dismissal in view controllers that require an explicit cancel/save action if they are not at the top of a navigation stack.
This is my first ever answer/solution. I hope it can help...
public class CustomPresentationControllerDelegate : UIAdaptivePresentationControllerDelegate
{
private Func<Task> dismissModal;
public CustomPresentationControllerDelegate(Func<Task> disposeViewAsync)
{
dismissModal = disposeViewAsync;
}
[Export("presentationControllerDidDismiss:")]
public override void DidDismiss(UIPresentationController presentationController)
{
dismissModal();
}
}
public override void WillMoveToParentViewController(UIViewController parent)
{
base.WillMoveToParentViewController(parent);
parent.PresentationController.Delegate = new CustomPresentationStyleDelegate(DismissModalAsync);
}
public async Task DismissModalAsync()
{
await this.Element.Navigation.PopModalAsync();
}
That's it!
I have a slightly different problem.
In my case if I close the modal windows by the new swipe down gesture, the navigation bar plain button that opened the modal windows in the first place stays disabled...
I tried @numan98 's solution, but the Element property is missing. It seems to be a Xamarin Forms workaround, while I'm on native. Otherwise it seemed to be compatible, so replaced the 'PopModalAsync' part with 'MvxIosViewPresenter.CloseModalViewController' but to no avail.
This is what i did to solve this issue:
[MvxModalPresentation(ModalPresentationStyle = UIModalPresentationStyle.PageSheet)]
public partial class MyModalViewController : MvxViewController<MyModalViewModel>, IUIAdaptivePresentationControllerDelegate
{
[Export("presentationControllerDidDismiss:")]
public void DidDismiss(UIPresentationController presentationController)
{
ViewModel.NavigationService.Close(ViewModel);
}
}
You have to inject IMvxNavigationService into your ViewModel for this to work.
Most helpful comment
This is my first ever answer/solution. I hope it can help...
1. We need a custom UIAdaptivePresentationControllerDelegate
2. Attach the custom delegate to the parent ViewController (in the custom renderer class)
3. Call the necessary methods to PopModal the view
That's it!