I would like to display a UIViewController
modally and be able to see a blurred version of the view that presented it.
Following a number of similar questions such as this:
iOS 7 Translucent Modal View Controller
I have added a background to my controller's view that is based on the captured view of the presenting controller. The problem I am facing is that my app supports multiple orientations and when the modal view is presented and rotated, the underlying background image no longer matches.
I tried grabbing a fresh snapshot of the presenting viewController in didRotateFromInterfaceOrientation:
of the modal viewController, but it appears that the UI of the presenting viewController is not being updated and the resulting image is still the wrong orientation. Is there any way to force redrawing of a view that is being hidden by the modal one?
After long considerations, I have come up with a passable way to handle it. How well it will work depends a bit on the type of content you have in the presenting viewController
.
The general idea is to take not one, but two screenshots before presenting a new viewController
- one for portrait, one for landscape. This is achieved by changing the frames of the top viewController
and navigation bar (if applicable) to emulate a different orientation, taking the screenshot of the result, and changing it back. The user never sees this change on device, but the screen grab still displays a new orientation.
The exact code will depend on where you are calling it from, but the main logic is the same. My implementation runs from AppDelegate
because it is reused by several subclasses of UIViewController
.
The following is the code that will grab the appropriate screenshots.
// get references to the views you need a screenshot of
// this may very depending on your app hierarchy
UIView *container = [self.window.subviews lastObject]; // UILayoutContainerView
UIView *subview = container.subviews[0]; // UINavigationTransitionView
UIView *navbar = container.subviews[1]; // UINavigationBar
CGSize originalSubviewSize = subview.frame.size;
CGSize originalNavbarSize = navbar.frame.size;
// compose the current view of the navbar and subview
UIImage *currentComposed = [self composeForeground:navbar withBackground:subview];
// rotate the navbar and subview
subview.frame = CGRectMake(subview.frame.origin.x, subview.frame.origin.y, originalSubviewSize.height, originalSubviewSize.width);
// the navbar has to match the width of the subview, height remains the same
navbar.frame = CGRectMake(navbar.frame.origin.x, navbar.frame.origin.y, originalSubviewSize.height, originalNavbarSize.height);
// compose the rotated view
UIImage *rotatedComposed = [self composeForeground:navbar withBackground:subview];
// change the frames back to normal
subview.frame = CGRectMake(subview.frame.origin.x, subview.frame.origin.y, originalSubviewSize.width, originalSubviewSize.height);
navbar.frame = CGRectMake(navbar.frame.origin.x, navbar.frame.origin.y, originalNavbarSize.width, originalNavbarSize.height);
// assign the variables depending on actual orientations
UIImage *landscape; UIImage *portrait;
if (originalSubviewSize.height > originalSubviewSize.width) {
// current orientation is portrait
portrait = currentComposed;
landscape = rotatedComposed;
} else {
// current orientation is landscape
portrait = rotatedComposed;
landscape = currentComposed;
}
CustomTranslucentViewController *vc = [CustomTranslucentViewController new];
vc.backgroundSnap = portrait;
vc.backgroundSnapLandscape = landscape;
[rooVC presentViewController:vc animated:YES completion:nil];
The method composeForeground:withBackground:
is a convenience method that generates an appropriate background image based on two input views (navigation bar + view controller). Aside from composing the two view together, it does a bit more magic to make the result look more natural when rotating the presented viewController
. Specifically, it extends the screenshot to a 1024x1024 square and fills the extra space with a mirrored copy of the composed image. In many cases, once blurred this looks good enough since the animation of the views re-drawing for the orientation change is not available.
- (UIImage *)composeForeground:(UIView *)frontView withBackground:(UIView *)backView {
UIGraphicsBeginImageContextWithOptions(backView.frame.size, 0, 0);
[backView.layer renderInContext:UIGraphicsGetCurrentContext()];
// translation is necessary to account for the extra 20 taken up by the status bar
CGContextTranslateCTM(UIGraphicsGetCurrentContext(), frontView.frame.origin.x, frontView.frame.origin.y);
[frontView.layer renderInContext:UIGraphicsGetCurrentContext()];
CGContextTranslateCTM(UIGraphicsGetCurrentContext(), -frontView.frame.origin.x, -frontView.frame.origin.y);
// this is the core image, would have left it at this if we did not need to use fancy mirrored tiling
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// add mirrored sections
CGFloat addition = 256; // 1024 - 768
if (newImage.size.height > newImage.size.width) {
// portrait, add a mirrored image on the right
UIImage *horizMirror = [[UIImage alloc] initWithCGImage:newImage.CGImage scale:newImage.scale orientation:UIImageOrientationUpMirrored];
UIGraphicsBeginImageContextWithOptions(CGSizeMake(newImage.size.width+addition, newImage.size.height), 0, 0);
[horizMirror drawAtPoint:CGPointMake(newImage.size.width, 0)];
} else {
// landscape, add a mirrored image at the bottom
UIImage *vertMirror = [[UIImage alloc] initWithCGImage:newImage.CGImage scale:newImage.scale orientation:UIImageOrientationDownMirrored];
UIGraphicsBeginImageContextWithOptions(CGSizeMake(newImage.size.width, newImage.size.height+addition), 0, 0);
[vertMirror drawAtPoint:CGPointMake(0, newImage.size.height)];
}
// combine the mirrored extension with the original image
[newImage drawAtPoint:CGPointZero];
newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// for ios 6, crop off the top 20px
if (SYSTEM_VERSION_LESS_THAN(@"7")) {
UIGraphicsBeginImageContextWithOptions(CGSizeMake(newImage.size.width, newImage.size.height-20), NO, 0);
[newImage drawAtPoint:CGPointMake(0, -20)];
newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
return newImage;
}
The resulting landscape and portrait images can be blurred and tinted as desired, and set as background for the presented viewController
. Use willRotateToInterfaceOrientation:duration:
method of this viewController
to select the appropriate image.
Note: I have tried to reduce the amount of work done on images and graphics contexts as much as possible, but there is still a slight delay when generating the background (around 30-90 ms per composeForeground:withBackground:
iteration, depending on the content, on a vintage slow iPad 2). If you know of a way to further optimize or simplify the above solution, please share!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With