Ios – present modal form sheet over modal page sheet

iosipadmodalviewcontrolleruikit

On the iPad I display a modal view controller with the modalPresentationStyle UIModalPresentationPageSheet. This view controller presents another modal view controller using the modalPresentationStyle UIModalPresentationFormSheet.

So, the user sees the background view controller, the page sheet and the form sheet all on top of each other, since the form sheet is smaller than the page sheet. The presentation of the page sheet lets the background dim, so that it can't be interacted with. The form sheet, though, does not dim the page sheet on iOS 5, so that the user can still interact with the page sheet underneath. But I want the page sheet dim as well, so that the user hase to close the modal form sheet before he can interact with the page sheet again.

On iOS 4, this is the default behaviour, but on iOS 5 I couldn't find a way to achieve this. Do you have any suggestions?

Best Answer

I believe this to be a bug in iOS5. I have done some experimenting with presenting modal view controllers from other modals and it seems the second modal NEVER dims the screen underneath. I even setup a test project that allowed you to launch endless modals from each other and it seem every other modal doesn't dim or block touches as expected.

A quick NSLog on the UIWindow subviews shows us that while the drop shadow is being added appropriately the dimmingView is not. I'm working on a way to show my own dimming view. Will update this answer when I've found a way.

Window Subviews: (
    "<UILayoutContainerView: 0xf63e3c0; frame = (0 0; 768 1024); transform = [0, 1, -1, 0, 0, 0]; autoresize = W+H; layer = <CALayer: 0xf645640>>",
    "<UIDimmingView: 0xf683990; frame = (0 0; 768 1024); opaque = NO; layer = <CALayer: 0xf6836d0>>",
    "<UIDropShadowView: 0xf683130; frame = (64 64; 640 896); transform = [0, 1, -1, 0, 0, 0]; autoresize = W+H; layer = <CALayer: 0xf6831c0>>",
    "<UIDropShadowView: 0x292110; frame = (74 242; 620 540); transform = [0, 1, -1, 0, 0, 0]; autoresize = LM+RM+TM+BM; layer = <CALayer: 0x292150>>"
)

Solution Two: So in my final solution for animated look. Also I got to thinking about my first solution and its technically possible this would piss off Apple and cause a rejection since UIDimmingView is undocumented and we "touch it." I add a UIView with a background color the alpha we want to my viewController. then I animated it when I present the modal and I reverse the animation when the delegate of the second modal gets called. It looks pretty good to me. Maybe some timing and alpha tweaks to get it JUST right but its working and looks nice.

- (void)viewDidLoad 
{
    dim = [[UIView alloc] init];
    [dim setBackgroundColor:[UIColor colorWithWhite:0.0 alpha:0.35]];
    [dim setAlpha:0.0];
    [dim setUserInteractionEnabled:NO];
    [self.view addSubview:dim];
}

- (void)presentModal
{
    [self.view bringSubviewToFront:dim];
    [dim setFrame:self.view.frame];
    [UIView animateWithDuration:0.25 animations:^{
        [dim setAlpha:1.0];
    }];
}

- (void)modalDelegateFinished
{
    [UIView animateWithDuration:0.25 animations:^{
        [dim setAlpha:0.0];
    }];
}

Solution One:

Alright so this works but it isn't as animated as I'd like. However it does reuse whats already there so theres probably a plus for that.

- (void)viewDidAppear:(BOOL)animated
{
    // Add a gesture to dismiss the view if tapped outside.
    UIGesture *gesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tappedOutsideView:)];
    [gesture setNumberOfTapsRequired:1];
    [gesture setCancelsTouchesInView:NO];
    [self.view.window addGestureRecognizer:gesture];

    // Move the dimmingview to just below the dropshadow.
    UIView *dim = [self.view.window.subviews objectAtIndex:1];
    [self.view.window insertSubview:dim atIndex:2];
}

- (void)tappedOutsideView:(UITapGestureRecognizer *)sender
{
    if (sender.state == UIGestureRecognizerStateEnded) {
        CGPoint location = [sender locationInView:nil];

        if (![self.view pointInside:[self.view convertPoint:location fromView:self.view.window] withEvent:nil]) {
            // remove the gesture on the window
            [self.view.window removeGestureRecognizer:sender];

            // Move the dimmingview back where it belongs
            UIView *dim = [self.view.window.subviews objectAtIndex:2];
            [self.view.window insertSubview:dim atIndex:1];
        }
    }
}

Also as a failsafe its probably a good idea to the same stuff in the viewDidDisappear. My done button calls tappedOutside view so I know the gesture and dimmingView are always put right. But if you didn't do it that way you could put it in the viewDidDisappear.

- (void)viewDidDisappear:(BOOL)animated
{
    // remove the gesture on the window
    for (UIGestureRecognizer *gesture in self.view.window.gestureRecognizers) {
        [self.view.window removeGestureRecognizer:gesture];
    }

    // Move the dimmingview back where it belongs
    UIView *dim = [self.view.window.subviews objectAtIndex:2];
    [self.view.window insertSubview:dim atIndex:1];
}