4
votes

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?

2
I noticed this in iOS 5 and am seeing the same problem in iOS 6.arlomedia
I just submitted this as bug report 12540692.arlomedia

2 Answers

2
votes

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];
}
0
votes

It does seem like an iOS bug. Still there in iOS7.

Here's what I did to work around it (iOS7 tested). Interestingly, presenting a modal over a modal will not dim the presenter, but a third modal presented on top of those will.

Knowing this we can present a dummy before presenting the actual form sheet modal. This involves minimal code and is technically in line with general iOS design patterns. So risk of a future blowup should be minimal.

First to present the form sheet:

- (void)presentFormSheetModalOverThePageSheetModal
{
    //get the controller I'm trying to present and stick it in a nav controller - as usual
    UIViewController *myModalController = [UIViewController new];
    UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:myModalController];
    navController.modalPresentationStyle = UIModalPresentationFormSheet;

    //create an intermediate controller that will be presented first
    UIViewController *intermediateController = [UIViewController new];
    intermediateController.modalPresentationStyle = UIModalPresentationCurrentContext;
    intermediateController.view.backgroundColor = self.view.backgroundColor;

    //create a snapshot of self (presenting controller)
    UIView *navBar = [self.navigationController.navigationBar snapshotViewAfterScreenUpdates:YES];
    UIView *mainView = [self.view snapshotViewAfterScreenUpdates:YES];
    mainView.center = CGPointMake(mainView.center.x, mainView.center.y + navBar.frame.size.height);
    [intermediateController.view addSubview:navBar];
    [intermediateController.view addSubview:mainView];

    //two step presentation
    [self presentViewController:intermediateController animated:NO completion:^{
        [intermediateController presentViewController:navController animated:YES completion:nil];
    }];
}

Then when it's time to dismiss (from the presented modal):

- (void)dismissMySelf
{
    //again - two step process to dismiss the this controller and the intermediate controller
    UIViewController *originalPresenter = self.presentingViewController.presentingViewController;
    [self.presentingViewController dismissViewControllerAnimated:YES completion:^{
        [originalPresenter dismissViewControllerAnimated:NO completion:nil];
    }];
}

I had to tweak it since the snapshot is a static image and does not handle rotation, but in general it worked well for me. Hope it helps.