xamarin / Xamarin.Forms

Xamarin.Forms is no longer supported. Migrate your apps to .NET MAUI.
https://aka.ms/xamarin-upgrade
Other
5.63k stars 1.88k forks source link

ModalInPresentation flag in iOS not working #12301

Open btschumy opened 3 years ago

btschumy commented 3 years ago

Description

Setting the ModalInPresentation flag for a UIViewController should, according to Apple:

The default value of this property is NO. When you set it to YES, UIKit ignores events outside the view controller's bounds and prevents the interactive dismissal of the view controller while it is onscreen.

However, this flag does not seem to work in Xamarin Forms (all versions I've tried). If you set it YES, you can still dismiss the modal page by dragging it to the bottom of the screen. If the flag is set NO, you should be able to dismiss the page by tapping outside the pages boundary but this seems to fail as well.

I've created an example app at www.otherwise.com/ModalPresentation.zip that illustrates the problems. The example app uses a custom PageRenderer to set the ModalInPresentation flag in multiple places (since it wasn't really clear where it needs to be set).

Note, I've been told that this flag is working correctly in a native Xamarin iOS app. It seems to only fail in Xamarin Forms.

Note that this bug may be related to another problem I've reported in issue #12300. This refers to a crash when tapping outside the modal page and then dismissing the page.

Steps to Reproduce

  1. Download example app at www.otherwise.com/ModalPresentation.zip
  2. Run the app on an iPad (simulator or real device).
  3. Tap the Show Modal Panel button to display the modal panel. This panel is displayed using:
Navigation.PushModalAsync(new OTNavigationPage(new ModalPage()));
  1. The code as downloaded sets ModalInPresentation to YES, however you can still dismiss the panel by dragging down.

Expected Behavior

The panel should not be able to be dismissed by dragging down with this flag set to true.

Actual Behavior

It can be dismissed by dragging.

Basic Information

Screenshots

Reproduction Link

www.otherwise.com/ModalPresentation.zip

Workaround

None that I've found

jeffington commented 3 years ago

@btschumy Hi Bill, I have been having the same problem as you. Here's my work-around. I created an extension that walks up the UIViewController hierarchy and sets the ModalInPresentation property for every UIViewController. I call this extension from the ViewDidAppear method of a custom NavigationRenderer.

NOTE: It's possible that you can call this from other places in the ViewController lifecycle, but I have not tested that and I found that the ViewController didn't get wrapped in a ModalWrapper until pretty late in the lifecycle.

    public static class UIViewController_Extensions
    {

        public static void SetModalInPresentation(this UIViewController viewController, bool modalInPresentation)
        {

            viewController.ModalInPresentation = modalInPresentation;
            if (viewController.ParentViewController != null)
            {
                viewController.ParentViewController.SetModalInPresentation(modalInPresentation);
            }

        }

    }

I was able to find a work-around because of your observation that the ModalInPresentation property worked on Xamarin.iOS but not on Xamarin.Forms. It seems like the issue is that modal ViewControllers are wrapped in a class called ModalWrapper and it's possible that ModalInPresentation is not being bubbled up through the ViewControllers. I read that you can set ModalInPresentation on any modal page in a navigation stack for the modal behavior to be updated, and it's possible that there is a disconnect where the navigation stack is not structured how the iOS system is expecting.

Thanks for sharing your observations and I hope this helps!

aspeckt-112 commented 1 year ago

Hello from 2023!

I came across this problem in .NET MAUI. I've created a Frakenstien's monster of a fix using a suggestion here: https://github.com/dotnet/maui/issues/7174

In the iOS folder, I created the following class:

using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform;
using ContentView = Microsoft.Maui.Platform.ContentView;

namespace Corkboard.Mobile;

// https://github.com/dotnet/maui/issues/7174
public partial class AuthenticationPageHandler : PageHandler
{
    protected override ContentView CreatePlatformView()
    {
        _ = VirtualView ??
            throw new InvalidOperationException($"{nameof(VirtualView)} must be set to create a LayoutView");
        _ = MauiContext ?? throw new InvalidOperationException($"{nameof(MauiContext)} cannot be null");

        if (ViewController == null)
            ViewController = new AuthenticationPageHandlerViewController(VirtualView, MauiContext);

        if (ViewController is PageViewController pc && pc.CurrentPlatformView is ContentView pv)
            return pv;

        if (ViewController.View is Microsoft.Maui.Platform.ContentView cv)
            return cv;

        throw new InvalidOperationException(
            $"PageViewController.View must be a {nameof(Microsoft.Maui.Platform.ContentView)}");
    }
}

Also in the iOS folder, added the following class (thanks to @jeffington above for the extension method):

using Microsoft.Maui.Platform;
using UIKit;

namespace Corkboard.Mobile;

public class AuthenticationPageHandlerViewController : PageViewController
{
    public AuthenticationPageHandlerViewController(IView page, IMauiContext mauiContext) : base(page, mauiContext)
    {
    }

    public override void ViewDidAppear(bool animated)
    {
        base.ViewDidAppear(animated);
        this.SetModalInPresentation(true);
    }
}

public static class UIViewControllerExtensions
{
    public static void SetModalInPresentation(this UIViewController viewController, bool modalInPresentation)
    {
        // TODO Do I care if it's less than iOS 13?
        viewController.ModalInPresentation = modalInPresentation;
        if (viewController.ParentViewController != null)
        {
            viewController.ParentViewController.SetModalInPresentation(modalInPresentation);
        }
    }
}

And in my MauiProgram.cs:

        builder.ConfigureMauiHandlers(collection =>
        {
#if IOS
            collection.AddHandler(typeof(AuthenticationPage), typeof(AuthenticationPageHandler));
#endif
        });

I feel quite dirty. I'm not sure if there's a better way, but it does work.