How to prevent the flash when calling drawViewHierarchyInRect:afterScreenUpdates: on iOS 8? - ios

Under iOS 8 a call to UIView's
[self drawViewHierarchyInRect:self.bounds afterScreenUpdates:YES];
results in a 1 frame flash of the image. You can see it for a split second on the screen. It only happens when afterScreenUpdates parameter is YES.
This is my complete code to take screenshot:
UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, sf);
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSaveGState(ctx);
CGContextConcatCTM(ctx, [self.layer affineTransform]);
if ([self respondsToSelector:#selector(drawViewHierarchyInRect:afterScreenUpdates:)]) { // iOS 7+
[self drawViewHierarchyInRect:self.bounds afterScreenUpdates:YES];
} else { // iOS 6
[self.layer renderInContext:ctx];
}
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
CGContextRestoreGState(ctx);
UIGraphicsEndImageContext();
Is there a workaround?

Provide #3x launch images to be used in iPhone6 and iPhone6 plus and this problem goes away.

Here's a possible workaround that solved my issue.
Use CATransaction.completionBlock to call UIView drawViewHierarchyInRect:afterScreenUpdates with the screen updates flag set to NO. I used this to workaround the same problem. It is semantically equivalent to setting the flag to YES in my case since I'm only invoking it after the current CATransaction has finished and the screen is in the desired state. That said, in my case, I don't have any animations, so this is pretty much immediate and captures exactly what I want. If there were animations, this may not be appropriate.
For my case, I have a collection view that is a collection of files. Each item is rendered as a snapshot of what the file looks like once the user opens it. Some files cause the UI to have hundreds of layers. When these complicated views were being snapshotted, I would get a black screen flash; or the last/current animation to partially rerun. My collection view is w/in a navigation controller, I was seeing partial reruns of the pop animation. I also have noticed reruns of rotation animations. For simpler files, there was often no issue. I noticed this on my iPad Air (iOs 8.1) and on various simulators. I tried putting appIcons and launchImages in place but they had no effect.
Here was the original code. It initializes a view controller, then adds the controller's view to the a utility view.
UIViewController *vlController = [[ComplicatedViewController alloc]init];
UIView *innerView = vlController.view;
[v addSubview:innerView];
innerView.transform = CGAffineTransformMakeScale(scalar, scalar);
innerView.center = CGRectCenterPoint(v.bounds);
auto sz = v.bounds.size;
innerView.bounds = {{0,0}, {sz.width/scalar, sz.height/scalar}};
UIGraphicsBeginImageContextWithOptions(v.bounds.size, YES, 0);
[v drawViewHierarchyInRect:v.bounds afterScreenUpdates:YES];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[innerView removeFromSuperview];
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
[v addSubview:imageView];
The new code just moves the bottom half to the completion block of UIView animateWithDuration:completion. Notice that the duration of the UIView animation is 0, but that still seems to give that extra refresh cycle to update the screen with the new UI.
UIViewController *vlController = [[ComplicatedViewController alloc]init];
UIView *innerView = vlController.view;
[UIView animateWithDuration:0.0 animations:^{
[v addSubview:innerView];
innerView.transform = CGAffineTransformMakeScale(scalar, scalar);
innerView.center = CGRectCenterPoint(v.bounds);
auto sz = v.bounds.size;
innerView.bounds = {{0,0}, {sz.width/scalar, sz.height/scalar}};
} completion:^(BOOL finished) {
UIGraphicsBeginImageContextWithOptions(v.bounds.size, YES, 0);
BOOL drawResult = [v drawViewHierarchyInRect:v.bounds afterScreenUpdates:NO];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[innerView removeFromSuperview];
[self add:image forFile:v.filename withOrientation:v.orientation];
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
[v addSubview:imageView];
}];

Related

Faster way to take snapshots ios

I have a UICollectionView. I want to generate two snapshots for each objects.
Sample Code:
for (int index = 0; index < 2; index++) {
UIView *dummyView = [[UIView alloc] init];
dummyView.frame = some frame....,
dummyView.backgroundColor = [UIColor yellowColor];
UIGraphicsBeginImageContextWithOptions(dummyView.bounds.size, NO, [UIScreen mainScreen].scale);
[dummyView.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *outputImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
NSData *imgData= UIImageJPEGRepresentation(outputImage, 1.00f);
//Write file.
}
I have written the code in cellForItemAtIndexPath. If there is no image in the path, it will create automatically. So when I launch the application for the first time, the UI is not responsive since there are many snapshots are getting created. This slow down the user interaction. Is there any work around for this?
Use drawViewHierarchyInRect instead of drawInContext as it's much faster
See the iOS 7 example here: https://coderwall.com/p/56wcfq/fast-snapshot-any-views-on-ios-7

Where is the first good moment to take a screenshot in UIViewController

I would like to make a screenshot of my view when view is loaded. I use this method:
- (UIImage *)screenshot {
UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.opaque, 0.0f);
if( [self respondsToSelector:#selector(drawViewHierarchyInRect:afterScreenUpdates:)] ) {
[self drawViewHierarchyInRect:self.bounds afterScreenUpdates:YES];
} else {
[self.layer renderInContext:UIGraphicsGetCurrentContext()];
}
UIImage *screenshot = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return screenshot;
}
The screenshot method was tested and works well. I am using the method in my UIViewController in viewDidLoad. The problem is that the result is a grey image! It's mean that view is not loaded in viewDidLoad method. The same issue can be observe in viewDidAppear. So viewDidLoad and viewDidAppear are too early. I have made workaround in viewDidLoad method:
__weak MyUIViewController *weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^() {
UIImage* image = [weakSelf.view screenshot]
// do sth with image...
});
How to make a screenshot without dispatach_async ?
Try This:
- (UIImage *)screenshot {
UIGraphicsBeginImageContextWithOptions(self.frame.size, NO, [UIScreen mainScreen].scale);
[self.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
UIView-JTViewToImage project also usefull.
You could also try the snapshotViewAfterScreenUpdates method of UIView:
[self.view snapshotViewAfterScreenUpdates:YES];
Try this
New API has been added since iOS 7, that should provide efficient way of getting snapshot
snapshotViewAfterScreenUpdates: renders the view into a UIView with unmodifiable content
resizableSnapshotViewFromRect:afterScreenUpdates:withCapInsets : same thing, but with resizable insets
drawViewHierarchyInRect:afterScreenUpdates:
same thing if you need all subviews to be drawn too (like labels, buttons...)
Hope it helps.

Fake push and pop animation when replace UIView

For some reasons, I have to use addSubview and addChildViewController to replace view instead of push/pop view controller. The problem is that I want to fake exactly the animation when changing UIViewController (push/pop).
Here is my try:
In RootViewController.m
// switch controller view
currentController = nextViewController;
[self addChildViewController:currentController];
[self.view addSubview:currentController.view];
// pop - move down
[currentController.view setFrame:CGRectMake(self.view.frame.size.width, 0, currentController.view.frame.size.width, currentController.view.frame.size.height)];
[UIView animateWithDuration:0.5
animations:^{
[currentController.view setFrame:CGRectMake(self.view.frame.size.width, self.view.frame.size.height, currentController.view.frame.size.width, currentController.view.frame.size.height)];
[self addConstrainForView];
}];
//Push - move up
[currentController.view setFrame:CGRectMake(0, self.view.frame.size.height, currentController.view.frame.size.width, currentController.view.frame.size.height)];
[UIView animateWithDuration:0.5
animations:^{
[currentController.view setFrame:CGRectMake(0, 0, currentController.view.frame.size.width, currentController.view.frame.size.height)];
[self addConstrainForView];
}];
This code doesn't work as aspect because the next controller move from bottom to top (push), and when it is moving, there is a blank background behind it (root controller 's background).
So I need the correct way to fake push and pop animation (pull up & pull down). Any help would be appreciated.
I use this effect in an app. I wanted (and have) the look of an iPhone series of pick screens embedded into the edge of an iPad app. So the user presses a button from a list then the screen transitions, that looks like a UIViewController advancing, to more options, though it's all in a window on the edge (and the relevant content pops up on the right). Here's the code I use:
- (void)advanceLevel
{
UIImageView *screenShot = [self getTableScreenShot];
[self.tableView.superview addSubview:screenShot];
// Put screen shot over the whole thing with mask for iPhone style animation
TableViewMask *mask =
[[TableViewMask alloc] initWithFrame:CGRectMake(0, 0, 1024, 768) withImage:[Util TakeScreenshot]];
[self.tableView.superview addSubview:mask];
tableLevel++;
[self updateData];
[UIView animateWithDuration:.6 animations:^
{
screenShot.frame =
CGRectMake(self.tableView.frame.origin.x-self.tableView.frame.size.width, self.tableView.frame.origin.y,
self.tableView.frame.size.width, self.tableView.frame.size.height);
controller.toolbar.backButton.alpha = (tableLevel>1?1:0);
}
completion:^(BOOL finished)
{
[screenShot removeFromSuperview];
[mask removeFromSuperview];
[self updateUI];
}];
}
- (void)goBackLevel
{
if(tableLevel==1){ return; }
if(tableLevel==3 && isEditing==YES)
{
// Skip this screen if coming from higher screens
tableLevel--;
}
UIImageView *screenShot = [self getTableScreenShot];
[self.tableView.superview addSubview:screenShot];
// Put screen shot over the whole thing with mask for iPhone style animation
TableViewMask *mask = [[TableViewMask alloc] initWithFrame:CGRectMake(0, 0, 1024, 768) withImage:[Util TakeScreenshot]];
[self.tableView.superview addSubview:mask];
tableLevel--;
[self updateData];
[UIView animateWithDuration:.6 animations:^
{
screenShot.frame =
CGRectMake(self.tableView.frame.origin.x+self.tableView.frame.size.width, self.tableView.frame.origin.y,
self.tableView.frame.size.width, self.tableView.frame.size.height);
controller.toolbar.backButton.alpha = (tableLevel>1?1:0);
}
completion:^(BOOL finished)
{
[screenShot removeFromSuperview];
[mask removeFromSuperview];
[self updateUI];
}];
}
... here's the screen-shot taker:
+ (UIImage *)TakeScreenshot
{
CGSize imageSize = [[UIScreen mainScreen] bounds].size;
UIGraphicsBeginImageContextWithOptions(imageSize, NO, 0);
CGContextRef context = UIGraphicsGetCurrentContext();
// Iterate over every window from back to front
for (UIWindow *window in [[UIApplication sharedApplication] windows])
{
if (![window respondsToSelector:#selector(screen)] || [window screen] == [UIScreen mainScreen])
{
// -renderInContext: renders in the coordinate space of the layer,
// so we must first apply the layer's geometry to the graphics context
CGContextSaveGState(context);
// Center the context around the window's anchor point
CGContextTranslateCTM(context, [window center].x, [window center].y);
// Apply the window's transform about the anchor point
CGContextConcatCTM(context, [window transform]);
// Offset by the portion of the bounds left of and above the anchor point
CGContextTranslateCTM(context,
-[window bounds].size.width * [[window layer] anchorPoint].x,
-[window bounds].size.height * [[window layer] anchorPoint].y);
// Render the layer hierarchy to the current context
[[window layer] renderInContext:context];
// Restore the context
CGContextRestoreGState(context);
}
}
// Retrieve the screenshot image
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
.. and finally TableViewMask:
#implementation TableViewMask
UIImage *screenShotImage;
- (instancetype)initWithFrame:(CGRect)frame withImage:(UIImage *)_screenShotImage
{
self = [super initWithFrame:frame];
if(self)
{
self.backgroundColor = [UIColor clearColor];
screenShotImage = _screenShotImage;
[self setNeedsDisplay];
}
return self;
}
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
[screenShotImage drawInRect:rect];
CGRect intersection = CGRectIntersection( CGRectMake(10.2, 130, 299.6, 600), rect );
if( CGRectIntersectsRect( intersection, rect ) )
{
CGContextFillRect(context, CGRectMake(10, 130, 300, 600));
CGContextClearRect(context, intersection);
CGContextSetFillColorWithColor( context, [UIColor clearColor].CGColor );
CGContextFillRect( context, intersection);
}
}
#end
I left in some things that are unique to mine because you'll likely need similar calls. For example [self updateData] takes the state of the system into account to update the model and set any other state-based calls. There's also an exception when a screen should be skipped I've left in. You can safely remove those, though you'll probably need similar variants (since you're not really changing UIViewController's).
The only caveat I'd have with this is that unless you have a corner case like mine -- imitating an iPhone in a popped out view on an iPad or something similar -- it's probably better just to use real UIVewController's.
This article would seem relevant to what you are trying to do?
When to use addChildViewController vs pushViewController
It seems to suggest you can use transitionFromViewController:toViewController:duration:options:animations:completion: to animate transition between controllers you are managing yourself.

Merge a UIImageView with a UIView that contains different subviews

I have a UIImage (called image) that i want to merge with another UIView (called overlayView), that contains different subviews eg. UILabels, UIImageViews and so on, to make a new UIImage (called mergedImage). Here's my code until now:
-(UIImage *) mergeViewWithImage: (UIImage *) image{
//The mergedImage need to be the same size as the image
UIGraphicsBeginImageContext(image.size);
//I'm doing this to get the layer, when i renderInContext, other ways?
UIImageView *temp = [[UIImageView alloc] initWithImage:image];
//This is working, the view gets its new frame, but the subviews won't scale up
[overlayView setFrame:CGRectMake(0, 0, image.size.width, image.size.height)];
[temp.layer renderInContext:UIGraphicsGetCurrentContext()];
[overlayView.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *mergedImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
//Need to set the frame to the initial one
[overlayView setFrame:CGRectMake(0, 0, 320, 320)];
return mergedImage;
}
This screenshot is what I really want it to look like
what you see is a AVCaptureVideoPreviewLayer (320*320), in the square, and I have a UIView, called overlayView (320*320), on top. When i press the "Take"-button, I do certain scaling, cropping and so on with the AVCaptureStillImageOutput, then i run the mergeViewWithImage-method. At this point the image is 968*968, but the overlayView is still 320*320 (has a blue boarder, just to know where it is). When I setFrame: to the overlayView, the the subviews, eg. the example-image of Luca Toni in the left lower corner doesn't scale, as you see in this screenshot
This is what i get
So you see, the background image (a white wall) stays the same, and the overlayView did resize, but the image of Luca Toni is much smaller, it wont scale... Please, help.
I've tried a lot of things, like setting this:
//imageOne is the picture with Luca Toni...
[[overlayView imageOne] setAutoresizingMask: UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth];
The overlayView is made in the Interface Builder as a .xib, which is a UIView with a UIImageVIew (imageOne), and has its own UIVIew custom class (nothing here yet, only the reference to imageOne (Luca Toni)). The overlayView is initiated in the MainViewController, like this:
[self overlayView:[[OverlayView alloc] init]];
overlayView = [[[NSBundle mainBundle] loadNibNamed:#"OverlayView" owner:self options:nil] objectAtIndex:0];
and added to the main view like this:
[[self view] addSubview:overlayView];

move didFinishLaunchingWithOptions: method to viewcontroller method with button trigger

I had the following code working in AppDelegate.h/.m but cannot change it to work in ViewController with a button triggering it.
#implementation BSViewController
#synthesize imageView;
#synthesize workingImage;
- (IBAction) chooseImage:(id) sender {
UIImage* testCard = [UIImage imageNamed:#"ipad 7D.JPG"];
CGImageRef num = CGImageCreateWithImageInRect([testCard CGImage],CGRectMake(532, 0, 104, 104));
UIGraphicsBeginImageContext(CGSizeMake( 250,650));
CGContextRef con = UIGraphicsGetCurrentContext();
UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
CGContextDrawImage(con, CGRectMake(0, 0, 13, 13) ,num);
UIGraphicsEndImageContext();
CGImageRelease(num);
UIImageView* iv = [[UIImageView alloc] initWithImage:im];
[self.imageView addSubview: iv];
iv.center = self.imageView.center;
[iv release];
I see the button named "Choose image" in the simulator, but I do not see the image there.
You haven't added self.imageView to any views.
You need
[self.view addSubview:self.imageView];
To show your imageView.
I was able to make it work, but let me ask you some dummy questions before. You can skip these and jump directly to the end for the answer:
Did it worked before?
Why aren't you using ARC?
Have you linked the chooseImage: to the button with Interface Builder, or programmatically? I sometimes forget this basic step, and nothing works of course :)
Why are you adding iv as a subview of self.imageView instead of just changing the image property of self.imageView. Knowing this could be useful to answer properly.
Supposing you have a good reason for adding iv as a subview, beware iv.center = self.imageView.center the centers are in two different coordinates system.
Form the Apple Documentation:
The center is specified within the coordinate system of its superview
and is measured in points. Setting this property changes the values of
the frame properties accordingly.
Edit: Other things to check
Does self.imageView exist? I mean, have you added it with IB and linked it to the IBOutlet, or defined it programmatically?
Add a break point in chooseImage: and go through it step by step, making sure that the objects you generate there are not nil
Test: try self.imageView setImage:im instead of self.imageView addSubview:iv, even if it's not what you want to do, if everything else is ok you should see the image.
It works if you switch the order of
UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
and
CGContextDrawImage(con, CGRectMake(0, 0, 13, 13) ,num);
Otherwise you would be asking to create im with the content of the context without nothing in drawn in it.
The code would then become:
UIImage* testCard = [UIImage imageNamed:#"ipad 7D.JPG"];
CGImageRef num = CGImageCreateWithImageInRect([testCard CGImage],CGRectMake(532, 0, 104, 104));
UIGraphicsBeginImageContext(CGSizeMake( 250,650));
CGContextRef con = UIGraphicsGetCurrentContext();
CGContextDrawImage(con, CGRectMake(0, 0, 13, 13) ,num);
UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
CGImageRelease(num);
Hope it helps. :D

Resources