I've implemented a custom (blurred) background for a modal view controller on iPad. Here's how I do it: in the presented VC's viewWillShow: I take a snapshot image of the presenting VC's view, blur it, and add a UIImage view on top of the presenting VC. This works well.
However, when I rotate the device while the modal is showing, the blurred image gets out of sync with the view controller it represents. The image gets stretched to fill the screen, but the view controller doesn't resize its views the same way. So when the modal is dismissed and the blur goes away, the transition looks bad.
I've tried taking another snapshot in didRotateFromInterfaceOrientation:, and replacing the blurred image, but that didn't work.
Any advice on how to solve this?
UPDATE: a sample project
My code (in the modal view controller):
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self drawBackgroundBlurView];
}
- (void)drawBackgroundBlurView
{
if(_blurModalBackground) {
[_backgroundBlurView removeFromSuperview];
CGRect backgroundFrame = self.presentingViewController.view.bounds;
_backgroundBlurView = [[UIImageView alloc] initWithFrame:backgroundFrame];
_backgroundBlurView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[_backgroundBlurView setImage:[STSUtils imageByBlurringView:self.presentingViewController.view]];
[self.presentingViewController.view addSubview:_backgroundBlurView];
}
}
Utils code
+ (UIImage *)imageByBlurringView:(UIView *)view
{
UIWindow *window = [[[UIApplication sharedApplication] windows] lastObject];
BOOL isPortrait = (UIInterfaceOrientationIsPortrait([[UIApplication sharedApplication] statusBarOrientation]));
CGRect frame;
if(isPortrait) {
frame = CGRectMake(view.frame.origin.x, view.frame.origin.y, view.frame.size.height, view.frame.size.width);
}
else {
frame = CGRectMake(view.frame.origin.x, view.frame.origin.y, view.frame.size.width, view.frame.size.height);
}
UIGraphicsBeginImageContextWithOptions(frame.size, NO, window.screen.scale);
[view drawViewHierarchyInRect:frame afterScreenUpdates:NO];
// Get the snapshot
UIImage *snapshotImage = UIGraphicsGetImageFromCurrentImageContext();
// Apply blur
UIColor *tintColor = [UIColor colorWithWhite:1.0 alpha:0.3];
UIImage *blurredSnapshotImage = [snapshotImage applyBlurWithRadius:8
tintColor:tintColor
saturationDeltaFactor:1.8
maskImage:nil];
UIGraphicsEndImageContext();
return blurredSnapshotImage;
}
I coded a simple example that takes a screenshot from presentingViewController and sets it as background image. Here is the implementaion of my modal view controller. And it works well (both on viewWillAppear and handles rotation properly). Could you please also post STSUtils code?
#import "ScreenshoterViewController.h"
#interface ScreenshoterViewController ()
#property (weak, nonatomic) IBOutlet UIImageView *imageView;
- (IBAction)buttonBackPressed;
#end
#implementation ScreenshoterViewController
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self updateImage];
}
- (void)updateImage
{
UIGraphicsBeginImageContext(self.view.bounds.size);
CGContextRef context = UIGraphicsGetCurrentContext();
[self.presentingViewController.view.layer renderInContext:context];
UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
self.imageView.image = img;
}
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
[self updateImage];
}
- (IBAction)buttonBackPressed {
[self dismissViewControllerAnimated:YES
completion:NULL];
}
#end
Update
In your utils code just replace this
[view drawViewHierarchyInRect:frame afterScreenUpdates:NO];
with this
[view drawViewHierarchyInRect:frame afterScreenUpdates:YES];
From apple docs:
afterUpdates A Boolean value that indicates whether the snapshot
should be rendered after recent changes have been incorporated.
Specify the value NO if you want to render a snapshot in the view
hierarchy’s current state, which might not include recent changes.
So you were taking screenshot that did not included latest changes in the view.
Related
I'm cross-dissolving between images on the pages of my UIPageController subclass. It is possible for a page to be turned while the transition is being applied. On occasion, the page being turned will become transparent mid-flip until the transition completes.
In the code below, the Page class is a subclass of UIViewController whose view has a UIImageView subview. PageController is my UIPageViewController subclass.
#implementation PageController : UIPageViewController
- (void)crossfadeToImage:(UIImage *)image
{
for (int i = 0; i < self.pages.count; i++) {
Page *page = self.pages[i]; // Pages fed to page controller in delegate
[UIView transitionWithView:page.view duration:duration options:UIViewAnimationOptionTransitionCrossDissolve | UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionCurveEaseInOut animations:^{
page.image = image;
} completion:nil];
}
}
Page code goes as follows:
// HEADER
#interface Page : UIViewController
#property (strong, nonatomic) UIImage *image;
- (instancetype)initWithPageFrameSize:(CGSize)size image:(UIImage *)image;
#end
// IMPLEMENTATION
#implementation Page
- (void)loadView
{
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, _pageFrameSize.width, _pageFrameSize.height)];
self.view = view;
}
- (UIImage *)image
{
return _imageView.image;
}
- (void)setImage:(UIImage *)image
{
if (!_imageView) {
_imageView = [[UIImageView alloc] initWithImage:image];
_imageView.frame = self.view.bounds;
[self.view addSubview:_imageView];
} else {
_imageView.image = image;
}
}
#end
I'm guessing the crossfade and the page curl animation are stacking in a weird way or something. Any ideas on how to prevent this "disappearing page" side-effect?
Turns out applying the animation to the imageView directly fixes this. Something like:
- (void)crossfadeToImage:(UIImage *)image
{
for (int i = 0; i < self.pages.count; i++) {
Page *page = self.pages[i]; // Pages fed to page controller in delegate
[UIView transitionWithView:page.**imageView** duration:duration options:UIViewAnimationOptionTransitionCrossDissolve | UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionCurveEaseInOut animations:^{
page.**imageView**.image = image;
} completion:nil];
}
}
Still, any insights as to why it doesn't work the other way are welcome.
I want to change to another tab but i also want to open it from bottom. Like a vertical transition.
I was trying to do like this :
-(void)test{
UIView * toView = [[self.tabBarController.viewControllers objectAtIndex:1] view];
UIImage *img=[self imageWithView:toView];
[UIView transitionWithView:img
duration:1.0f
options:UIViewAnimationOptionCurveEaseIn
animations:^{
self.tabBarController.selectedIndex = 0;
} completion:NULL];
}
- (BOOL)tabBarController:(UITabBarController *)tabBarController
shouldSelectViewController:(UIViewController *)viewController{
return false;
}
- (UIImage *) imageWithView:(UIView *)view
{
UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.opaque, 0.0);
[view.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage * img = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return img;
}
but it's not the effect that i want . Any ideea?
This is my solution:
Firstly. return false in the UITabBarController' delegate. So the system won't change the tab immediately when user selecting the tab.
- (BOOL)tabBarController:(UITabBarController *)tabBarController
shouldSelectViewController:(UIViewController *)viewController
Secondly. Capture your toView to an image
Thirdly. Do any animation you want with the imageView.
Finally. Set selectedIndex of your tabBarController and remove the imageView when the animation completed.
Feel free to ask me. Thanks!
I have self view with image view:
UIImageView *imageView = [[UIImageView alloc] initWithFrame:self.bounds];
[self setBluredImageView:imageView];
[self addSubview:imageView];
- (UIImage *)takeSnapshotOfView:(UIView *)view
{
CGFloat reductionFactor = 1;
UIGraphicsBeginImageContext(CGSizeMake(view.frame.size.width/reductionFactor, view.frame.size.height/reductionFactor));
[view drawViewHierarchyInRect:CGRectMake(0, 0, view.frame.size.width/reductionFactor, view.frame.size.height/reductionFactor) afterScreenUpdates:YES];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
In method when I want to show my self view over other view I make this:
- (void)showMe; {
AppDelegate* app = [AppDelegate shared];
[app.window addSubview:self];
UIImage *image = [self blurWithImageEffects:[self takeSnapshotOfView:app.window]];
[[self bluredImageView] setImage:image];
[UIView animateWithDuration:0.4 animations:^{
[self setAlpha:1.0];
}];
}
So as you can see I want to blur "graphic context" that based on main window view. At first time when I present my self view it works perfect, but then, something like blurred image multiply each other.
So this is image when I just first show my view:
When I present my view few times the blurred image looks like this:
So as you can see each time blurred screenshot is different, but I use the same method for getting screenshot and don't update content of the view controller or other ui parts.
Some methods and image categories found here.
You've created a loop of blurring. When you take the screenshot of the view a second time, the bluredImageView is also in the screenshot. That is why you see the effect multiplied. Try removing it and only capturing the context without the effect, then adding it back
- (UIImage *)takeSnapshotOfView:(UIView *)view
{
//Remove the blured image before taking another screenshot.
[self bluredImageView] removeFromSuperview];
CGFloat reductionFactor = 1;
UIGraphicsBeginImageContext(CGSizeMake(view.frame.size.width/reductionFactor, view.frame.size.height/reductionFactor));
[view drawViewHierarchyInRect:CGRectMake(0, 0, view.frame.size.width/reductionFactor, view.frame.size.height/reductionFactor) afterScreenUpdates:YES];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
//Add it back now that the effect is done
[self addSubview:[self bluredImageView];
return image;
}
I have a requirement to show a status bar at certain times at the bottom of my application. I can easily put this at the bottom of my application's main view, but whenever I push a view controller on top of this (either modally or not) it hides this status bar.
Is there any way I can add a status bar like this, and have it be outside the bounds of my application itself? Ideally I'd like this to work like the call-in-progress status bar on the iPhone - when this bar appears, the app is pushed down, and a call to [[UIScreen mainScreen] applicationFrame] returns the correct size (i.e. it accounts for the presence of this status bar when calculating the height available for the app).
I wanted to do this, too, so I tried View Controller Containment. I'm still trying it out, so I'm not willing to give this a ringing endorsement, but it might be something you'd want to try playing around with yourself if you're in iOS5. But it appears to give you a status bar that will appear or disappear from the bottom of the screen.
This is a view controller that will open another view controller, but if there is status text to show, it pops up from the bottom of the screen and stays there until you get rid of it. I've only done a little testing so far, but it looks like this handles pushViewController/popViewController, but maybe not modal views.
My header looks like:
// StatusBarViewController.h
//
// Created by Robert Ryan on 7/8/12.
#import <UIKit/UIKit.h>
#interface StatusBarViewController : UIViewController
#property (strong, nonatomic) UIViewController *appController;
- (void)setStatus:(NSString *)text;
#end
My implementation file (this is ARC) looks like:
// StatusBarViewController.m
//
// Created by Robert Ryan on 7/8/12.
#import "StatusBarViewController.h"
#interface StatusBarViewController ()
{
BOOL _statusHidden;
UIView *_appView;
UILabel *_statusLabel;
}
#end
#implementation StatusBarViewController
#synthesize appController = _appController;
- (void)dealloc
{
_appView = nil;
_statusLabel = nil;
[self setAppController:nil]; // usually I don't like setters in dealloc, but this does some special stuff
}
- (void)createControlsWithStatusHidden
{
// create default app view that takes up whole screen
CGRect frame = self.view.frame;
frame.origin = CGPointMake(0.0, 0.0);
_appView = [[UIView alloc] initWithFrame:frame];
_appView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_appView.clipsToBounds = YES;
[self.view addSubview:_appView];
// create status label that is just off screen below the app view
_statusLabel = [[UILabel alloc] init];
_statusLabel.font = [UIFont fontWithName:#"Helvetica-Bold" size:12.0];
_statusLabel.backgroundColor = [UIColor darkGrayColor];
_statusLabel.textColor = [UIColor whiteColor];
CGSize size = [#"Hey!" sizeWithFont:_statusLabel.font]; // test size of box with random text
_statusLabel.frame = CGRectMake(0.0, frame.size.height, frame.size.width, size.height);
_statusLabel.textAlignment = UITextAlignmentCenter;
_statusLabel.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleWidth;
[self.view addSubview:_statusLabel];
}
- (void)viewDidLoad
{
[super viewDidLoad];
[self createControlsWithStatusHidden];
_statusHidden = YES;
// I'm instantiating from storyboard. If you're using NIBs, just create your controller controller using initWithNib and then set our appController accordingly.
self.appController = [self.storyboard instantiateViewControllerWithIdentifier:#"MainNavigator"];
}
- (void)setAppController:(UIViewController *)controller
{
if (controller)
{
controller.view.frame = CGRectMake(0.0, 0.0, _appView.frame.size.width, _appView.frame.size.height);
[self addChildViewController:controller];
[controller didMoveToParentViewController:self];
if (self.appController)
{
// if we have both a new controller and and old one, then let's transition, cleaning up the old one upon completion
[self transitionFromViewController:self.appController
toViewController:controller
duration:0.5
options:UIViewAnimationOptionTransitionCrossDissolve | UIViewAnimationOptionCurveEaseInOut
animations:nil
completion:^(BOOL finished){
if (self.appController)
{
[self.appController willMoveToParentViewController:nil];
[self.appController removeFromParentViewController];
}
}];
}
else
{
// if we have no previous controller (i.e. this is our first rodeo), then just add it to the view
[_appView addSubview:controller.view];
}
}
else
{
// no new controller, so we're just removing any old on if it was there
if (self.appController)
{
// if there was an old controller, remove it's view, and remove it from the view controller hierarchy
[self.appController.view removeFromSuperview];
[self.appController willMoveToParentViewController:nil];
[self.appController removeFromParentViewController];
}
}
_appController = controller;
}
- (void)hideStatusWithCompletion:(void (^)(BOOL finished))completion
{
[UIView animateWithDuration:0.25
animations:^{
CGRect labelFrame = _statusLabel.frame;
labelFrame.origin.y += labelFrame.size.height;
_statusLabel.frame = labelFrame;
CGRect appFrame = _appView.frame;
appFrame.size.height += labelFrame.size.height;
_appView.frame = appFrame;
}
completion:completion];
}
- (void)unhideStatusWithCompletion:(void (^)(BOOL finished))completion
{
[UIView animateWithDuration:0.25
animations:^{
CGRect labelFrame = _statusLabel.frame;
labelFrame.origin.y -= labelFrame.size.height;
_statusLabel.frame = labelFrame;
CGRect appFrame = _appView.frame;
appFrame.size.height -= labelFrame.size.height;
_appView.frame = appFrame;
}
completion:completion];
}
- (void)setStatus:(NSString *)text
{
BOOL hasText = (text && [text length] > 0);
if (hasText)
{
if (!_statusHidden)
{
// if we have text, but status is already shown, then hide it and unhide it with new value
[self hideStatusWithCompletion:^(BOOL finished){
_statusLabel.text = text;
[self unhideStatusWithCompletion:nil];
}];
}
else
{
// if we have text, but no status is currently shown, then just unhide it
_statusLabel.text = text;
[self unhideStatusWithCompletion:nil];
}
_statusHidden = NO;
}
else
{
if (!_statusHidden)
{
// if we don't have text, but status bar is shown, then just hide it
[self hideStatusWithCompletion:^(BOOL finished){
_statusLabel.text = text;
}];
_statusHidden = YES;
}
}
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
}
#end
And then, any view controller that wants to update the status message would use a method kind of like:
- (void)setStatus:(NSString *)text
{
UIViewController *controller = [UIApplication sharedApplication].delegate.window.rootViewController;
if ([controller isKindOfClass:[StatusBarViewController class]])
{
[(StatusBarViewController *)controller setStatus:text];
}
}
i have 3 navigation controller and i want to change each background using different image. I was implement a category that extends UINavigationBar like this :
- (void)drawRect:(CGRect)rect {
UIImage *image = [UIImage imageNamed:#"background.png"];
[image drawInRect:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)];}
#end
but it make every navigation bar have a same background image. And then i try to implement code like this :
- (void)viewDidLoad {
[super viewDidLoad];
self.navigationController.navigationBar.tintColor = [UIColor blackColor];
UIImageView *backGroundView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"background.png"]];
[self.navigationController.navigationBar insertSubview:backGroundView atIndex:0];
[backGroundView release];
}
in every controller but each background just show the tintColor, not the image...what should I do???
and how if i want to do that too in tabbarController??
On each of your views, implement the following in your viewWillAppear: method
#import <QuartzCore/QuartzCore.h>
...
- (void)viewWillAppear:(BOOL)animated
{
self.navigationController.navigationBar.layer.contents = (id)[UIImage imageNamed:#"yourImageBgHere"].CGImage;
}
Cheers,
Rog