dotnet / maui

.NET MAUI is the .NET Multi-platform App UI, a framework for building native device applications spanning mobile, tablet, and desktop.
https://dot.net/maui
MIT License
21.97k stars 1.71k forks source link

Unable to use UIAdaptivePresentationControllerDelegate events for modal pages #20473

Open Axemasta opened 6 months ago

Axemasta commented 6 months ago

Description

If I show a modal page on iOS with ModalPresentationStyle set to PageSheet, or any presentation where the page can be dismissed via a gesture, there is no event to hook into to know that page has been dismissed at the maui level (as far as I am aware).

This leads to a situation where frameworks like Prism don't detect page transitions and can get stuck. MVVMCross had this issue in Xamarin Forms and fixed it here. I have tried porting this code to my maui app and the UIAdaptivePresentationControllerDelegate dismiss method is never fired.

I have tried setting it in 2 ways, 1 inheriting the NavigationHandler and setting the PresentationController delegate:

public class DismissAwareNavigationPageHandler : NavigationRenderer
{
    public override void ViewDidLoad()
    {
        Action dismissAction = () =>
        {
            Debug.WriteLine("Presentation controller did dismiss (UINavigationController)");
        };

        if (PresentationController != null)
        {
            Debug.WriteLine("Adding dismiss aware presentation delegate to navigation page");
            PresentationController.Delegate = new DismissAwareUIPresentationControllerDelegate(dismissAction);
        }

        base.ViewDidLoad();
    }
}

Another way I tried was using a content page renderer and setting a view controller which would set this delegate:

public class DismissAwarePopoverPageHandler : PageHandler
{
    class PopoverPageViewController(IView page, IMauiContext mauiContext) : PageViewController(page, mauiContext)
    {
        public override void ViewDidLoad()
        {
            Debug.WriteLine("Handler view controller connected");

            if (PresentationController != null)
            {
                Action dismissAction = () =>
                {
                    Debug.WriteLine("Presentation controller did dismiss (UIViewController)");
                };

                Debug.WriteLine("Attaching UIAdaptivePresentationControllerDelegate to parent navigation controller");
                PresentationController.Delegate = new DismissAwareUIPresentationControllerDelegate(dismissAction);
            }

            base.ViewDidLoad();
        }

        public override void WillMoveToParentViewController(UIViewController? parent)
        {
            base.WillMoveToParentViewController(parent);

            if (parent is NavigationRenderer && parent?.NavigationController?.PresentationController != null)
            {
                Action dismissAction = () =>
                {
                    Debug.WriteLine("Presentation controller did dismiss (UIViewController parent UINavigationController)");
                };

                Debug.WriteLine("Attaching UIAdaptivePresentationControllerDelegate to parent navigation controller via child uiviewcontroller");
                parent.NavigationController.PresentationController.Delegate = new DismissAwareUIPresentationControllerDelegate(dismissAction);
            }

        }
    }

    protected override Microsoft.Maui.Platform.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 PopoverPageViewController(VirtualView, MauiContext);

        if (ViewController is PageViewController pc && pc.CurrentPlatformView is Microsoft.Maui.Platform.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)}");
    }
}

In my reproduction I provide a native swift example where the code to observe the dismiss events is super easy:

class ModalViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.

        if (navigationController != nil) {
            self.navigationController?.presentationController?.delegate = self
        }
        else {
            self.presentationController?.delegate = self
        }
    }
}

extension ModalViewController : UIAdaptivePresentationControllerDelegate {

    func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
        print("Modal was dismissed")
    }
}

I am looking to implement this functionality in maui, since its not working I wonder if it is an issue with the .NET ios sdk, otherwise is there a better way to set this delegate where it would actually work.

Steps to Reproduce

Link to public reproduction project repository

https://github.com/Axemasta/DismissPresentationControllerRepro

Version with bug

8.0.6

Is this a regression from previous behavior?

Yes, this used to work in Xamarin.Forms

Last version that worked well

Unknown/Other

Affected platforms

iOS

Affected platform versions

iOS 13+

Did you find any workaround?

No

Relevant log output

No response

Axemasta commented 6 months ago

I've updated my repro with a .NET for iOS sample of this problem, and it works just like the swift example so the SDK is find, this is an issue specifically in Maui.

Whats strange is I've updated my maui code so that when WillMoveToParentViewController is called, I grab the NavigationRenderer from that parent view controller, so I definitely have the top level UINavigationController for the stack & set the delegate, it still is not called.

Any ideas on what could be happening here @PureWeen ?

Axemasta commented 6 months ago

Looks like this was an issue in Xamarin ([Bug] Page not popped on iOS 13 FormSheet swipe down).

I wonder if the reason I can't set the delegate manually is because its the wrong view controller and one of the wrapper vc's is actually what recieves the dismiss event.

In my Prism app, when I swipe the popover closed (not hard close it), the modal stack count will be set to 1 instead of 0, so this is the same bug in effect.

The following workaround suggested for forms doesn't work here, you get an infinite loop:

protected override async void OnDisappearing()
{
    base.OnDisappearing();

    while (Navigation.ModalStack.Count > 0)
    {
        Debug.WriteLine("Popping orphaned modal");
        await Navigation.PopModalAsync(false);
    }   
}
kevinxufei commented 5 months ago

Verified this issue with Visual Studio 17.10.0 preview2(8.0.14/8.0.3). Can repro on iOS platform with sample project.