How to force -drawViewHierarchyInRect:afterScreenUpdates: to take snapshots at #2x resolution? - ios

-drawViewHierarchyInRect:afterScreenUpdates: is a fast way in iOS 7 to take a snapshot of a view hierarchy.
It takes snapshots but with #1x resolution. The snapshots look pixellated and blurry on an iPhone 5S. The view from which I create a snapshot is not transformed.
I don't want to blur it and want good quality as seen on screen.
Here is how I capture it:
UIGraphicsBeginImageContext(self.bounds.size);
[self drawViewHierarchyInRect:self.bounds afterScreenUpdates:YES];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
I also tried:
UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, self.contentScaleFactor);
which still renders with low #1x quality.
Is there another way to configure the image context so it's #2x resolution?

The correct one would be:
UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0.0f);

OK, so the problem was that self.contentScaleFactor returns a wrong value on a retina display device. It is 1 where it should be 2.
This works, but of course it's less than ideal because it has no fallback for non-retina devices.
UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 2);

Related

"[CALayer renderInContext]" crashes on iPhone X

I have a custom UIView that I want to render as an UIImage. The custom UIView is a subclass of UIImageView.
Inside this view, I am rendering some UI elements (drawing a bunch of circles over the image). The number of circles added can go up to the order of thousands.
I am using this simple code snippet to render the view as an UIImage:
// Create the UIImage (code runs on the main thread inside an #autorelease pool)
UIGraphicsBeginImageContextWithOptions(viewToSave.image.size, viewToSave.opaque, 1.0);
[viewToSave.layer renderInContext:UIGraphicsGetCurrentContext()];
imageToSave = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// Since I'm rendering some stuff inside the .layer of the UIView,
// I don't think I can use "drawViewHierarchyInRect: afterScreenUpdates:"
Here is the memory allocations taken from Instruments, in an example with ~3000 circles added as sub-views:
Now here's the strange part... This runs fine and I can render the image multiple times (consecutive) and save it in the image gallery on devices like iPhone 5, iPhone 5s, iPhone 6s, iPad Air 2, iPad Mini 4... But the same code triggers a memory warning on iPhone X and eventually crashes the application...
Unfortunately, I do not have access to an iPhone X and the person who reported this doesn't have access to a Mac, so I cannot investigate deeper.
I really don't know if I am doing anything wrong... Are you aware if there is something different about the iPhone X? I've been struggling this issue for quite a while...
I guess that the problem has to do with how CALayer:renderInContext: handles drawing of thousands of views in a context that requires them to be scaled up. Would it be possible to try render the sub-views yourself? Then compare and verify if it works better by using instrumentation.
UIImage *imageToSave = [self imageFromSubLayer:viewToSave];
- (UIImage *)imageFromSubLayers:(UIImageView *)imageView {
CGSize size = imageView.image.size;
UIGraphicsBeginImageContextWithOptions(size, YES, .0);
CGContextRef context = UIGraphicsGetCurrentContext();
for (CALayer *layer in imageView.layer.sublayers)
[layer renderInContext:context];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
As allways, the answer lays in the smallest (and not included in the question) detail...
What I really didn't consider (and found out after quite a while), is that the iPhone X has a scale factor equal to #3x. This is the difference between the iPhone X and all other devices that were running the code just fine...
At some point in my code, I was setting the .contentScaleFactor of the subviews to be equal to [UIScreen mainScreen].scale. This means that on higher-end devices, the image quality should be better.
For the iPhone X, [UIScreen mainScreen].scale returns 3.
For all of the other devices I have tested with, [UIScreen mainScreen].scale returns 2. This means that on the iPhone X, the ammount of memory used to render the image is way higher.
Fun fact number two: From another useful SO post, I found out that on the iPhone X, if you try to allocate more than 50% of it's total amount of memory (1392 MB), it crashes. On the other hand, for example, in the case of the iPhone 6s, the percentage is higher: 68% (1396 MB). This means that for some older devices you have more meory to work with than on the iPhone X.
Sorry for missleading, it was a honest mistake from my part. Thank you all for your answers!
I recently wrote a method into an app I'm making to convert UIView's into UIImages (so i could display gradients on progress views/tab bars). I ended up settling with the following code, I'm using this code to render tab bar buttons, it works on all devices including the X.
Objective C:
UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:gradientView.bounds.size];
UIImage *gradientImage = [renderer imageWithActions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext) {
[gradientView drawViewHierarchyInRect:gradientView.bounds afterScreenUpdates:true];
}];
Swift 4:
let renderer = UIGraphicsImageRenderer(size: gradientView.bounds.size)
let image = renderer.image { ctx in
gradientView.drawHierarchy(in: gradientView.bounds, afterScreenUpdates: true)
}
I used this code in a sample project I wrote up, here is a link to the project files:
Swift,
Objective C
as you will see both projects will run on iPhone X perfectly!
I know, the following sounds weird. But try to make the target image one pixel larger than the one that you are drawing. This solved it for me (my particular problem: "[CALayer renderInContext]" crashes on iPhone X).
In code:
UIGraphicsBeginImageContextWithOptions(
CGSizeMake(viewToSave.image.size.width + 1,
viewToSave.image.size.height + 1),
viewToSave.opaque,
1.0
);

UIImage resize performance and quality issue

I'm working with UIImage and like everyone else have to deal with retina and non-retina display adaptability. As for as I know, retina display requires double pixels.
I'm wondering if I could simply use a large image with the same width/height ratio, just resize it smaller to adapt all device?
For example, I made a original image with size of 200*200 pixel. Now I want to use it in application as 20*20 pixel, and 80*80 pixel (two situations). Then I have to make four copies like img2020.png, img2020#2x.png, img8080.png and img8080#2x.png
So if I want to use it in three situations with difference size, I have to store 6 copies. Can I just use UIImage's resize function to do this? I've tried a bit but cannot figure out it's quality and performance.
Any ideas? Thanks a lot :)
All native API suppose you to use image.png and image#2x.png, so it may be difficult sometimes to use just one image and scale it depending on retina/non-retina. Moreover using retina graphics on non-retina devices lead to more extensive use of these devices' resource causing battery drain. And, of course, if you have many images, that will decrease performance of your application. In other words there are reasons to use double set of images and you should better use it instead of one large image being scaled.
You don't need to make 6 copies. You should use the size 200*200 pixel. And set the property contentMode of imageview to aspectFit. Or you can also use below function and change the size of images at run time.
-(UIImage *)Resize_Image:(UIImage *)image requiredHeight:(float)requiredheight andWidth:(float)requiredwidth
{
float actualHeight = image.size.height;
float actualWidth = image.size.width;
if (actualWidth*requiredheight <actualHeight*requiredwidth)
{
actualWidth=requiredheight*(actualWidth/actualHeight);
actualHeight=requiredheight;
}
else
{
actualHeight=requiredwidth*(actualWidth/actualHeight); actualWidth=requiredwidth;
}
CGRect rect = CGRectMake(0.0, 0.0, actualWidth, actualHeight);
UIGraphicsBeginImageContext(rect.size);
[image drawInRect:rect];
UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return img;
}
I made some comparisons before. Leaving iOS handle the resizing causes lower quality, and really unacceptable sometimes.
I feel lazy sometimes, my approach is to run it with the retina version, and if it looks bad, I will create a low-res version.
If you're writing an iPhone-only app, most of iPhones on the market has retina, so I don't think you should worry about non-retina version. Just my opinion though.

iOS image size of retina image from web service

Suppose I want an image to be 320x100 points for a retina screen, I would have to make an image 640x200 pixel and named it #2x. The problem is when I download an image from a web service of size 640x200 pixel. Normally a #2x image would be translate to size 320x100 points in the phone, but the image from the web service is still 640x200 points.
Note - the web service is my own, so I can fix it if it's the web service's problem.
Sorry If might have not worded the problem well, but this is what I meant(similar):
Retina display and [UIImage initWithData]
You'll have to set the height and width of the control in points manually and it will automatically display the image with a higher DPI without unnecesarily downscaling.
An #2x image will always be twice the dimensions of a non-retina image if it wasn't loaded through imageNamed.
Create an image like so on a retina and non-retina device:
UIImage *anImage = [UIImage imageNamed: #"anImageName"];
NSLog(#"%#: scale: %f", NSStringFromCGSize(anImage.size), anImage.scale);
The CGSize object printed on the retina device will be the same size of the non-retina image, but will have the scale set to 2.0.
Creating an image using the explicit retina suffix will reveal that the image is actually twice as big - so imageNamed does its own image scaling.
For the case where you want to display this in an already created and sized image view, you still don't need to do anything - just load it straight in and the image view will adjust the image to the correct size.
If, however, you want to create a new image view, then you'll need to create a frame paying attention to the UIScreen's resolution like so (unfortunately, you can't just set the scale property as its read only):
CGRect newFrame = CGRectZero;
newFrame.size.width = (anImage.size.width / [UIScreen mainScreen].scale);
newFrame.size.height = (anImage.size.height / [UIScreen mainScreen].scale);
This assumes that your web service is aware of wether the screen of the device is retina or not; some services will pick this up automatically, some will require you to tell them this up front. YMMV.

iOS image sizes for iPad and iPhone

I have developed an small iOS app, where i have image named bg.png which is of dimension
1024 * 768 for iPad.
Now i have many images which has been created for iPad size. Now i need to make support of this app in iPhone, for that weather i need to create same set of images agian for iPhone size,
568 * 300 for iPhone.
or there is another way to do this?
Scaling down the iPad image assets will destroy UX on iPhone. Also images like icon, splash screen usually contain company logo. Scaling down will tamper the look of the logo and overall image. Better way is to create separate images for iPhone form factor. Trim the png files using http://tinypng.org/ to keep binary size low.
Cheers!Amar.
You can use this code to re-size the image by following code,
CGSize newSize = CGSizeMake(568, 300);
UIGraphicsBeginImageContext(newSize);
[yourIpadImage drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)];
newIphoneImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
+ (UIImage *)imageWithImage:(UIImage *)image scaledToSize:(CGSize)newSize {
//UIGraphicsBeginImageContext(newSize);
UIGraphicsBeginImageContextWithOptions(newSize, NO, 0.0);
[image drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)];
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImage;
}
You have option to change the Size of Your Image
sathiamoorthys solution is a difficult way or rescaling your image. You can do that by simply creating a UIImageView, initialize it with a UIImage and then change its frame.
Note that your image will look scaled/distorted that way.
follow this:
open the image in preview.
go to tools > adjust size
put in whatever size you want.
save the image as a different name.
yes
you should create duplicate and resize them for iphone. Using same images for iphone will bring memory issues because the images are unnecessarily big for iphone.
Use any software to resize them or you can do this using preview also as Nikita described above
If you are doing this to create universal app then you must postfix ~ipad in the name of the image file.
Please visit this link, May help you and solve your issue.
There is the some tips like:
Propotional scale,
Resize
If you want your images to show up unscaled, you are going to need an additional image with the correct size.
So supporting both iPad with and without retina screens would require one image of 768x1024 and one of 1536 x 2048. For iPhone 3.5" you would need 960 x 640 when it is a retina screen or 480 x 320 when it is non-retina. For iPhone 5 (4" screen) you would need 568 x 320.
If you use UIImages method imageNamed: there is help from Apple. It loads on retina devices that method looks for the the image you specified with the postfix '#2x'. So you can simply code:
UIImage * myImage = [UIImage imageNamed: #"myImage"]
If you make sure you project contains myImage.png for non-retina devices and myImage#2x.png for retina devices the right image gets loaded at runtime.

Drawing retina versus non-retina images

UIImage 1: Loaded from a file with the #2x modifier with size 400x400, thus UIImage 1 will report its size as 200x200
UIImage 2: Loaded from a file without the #2x modifier with size 400x400, thus UIImage 2 will report its size as 400x400
I then create 2 images from the above applying the code below to each
UIGraphicsBeginImageContextWithOptions(CGSizeMake(400,400), YES, 1.0);
[image drawInRect:CGRectMake(0, 0, 400, 400)];
UIImage *rescaledI = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
Considering the above, can I expect the image quality for both resulting images to be exactly the same? (I am trying to determine if drawing a 200x200 retina image to a 400x400 non-retina context will degrade quality versus drawing the same image not loaded as a retina image)
Just return the current image's size.
UIImage *image1 = [UIImage imagedNamed:#"myimage.png"];
//access width and height like this
image1.size.width;
image1.size.height;
UIGraphicsBeginImageContextWithOptions(CGSizeMake(image1.size.width, image1.size.height), YES, 1.0);
[image drawInRect:CGRectMake(0, 0, image1.size.width, image1.size.height)];
UIImage *rescaledI = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
Of course if you should replace image1 with whatever image you are trying to get the size of. Some switch statement or if statement should do the trick for you.
Never hardcode sizes/dimensions/locations etc. You should always pull that info dynamically by asking your image its size. Then you can change the size of your image without fear of having to locate the hardcoded size in your application.
The image is always 400*400 pixels: the difference is that in a retina display 400*400 pixels cover less space, that is exactly half (200*200 Core Graphics points). If you are not applying any transformation to the image will stay exactly the same.
The code you wrote renders the image as is because you are overriding the device scale factor and setting it to always 1 (1 pixel to 1 point).
You should use two images, one twice as big, if you want your image to cover the same amount of screen on both retina and non retina devices.

Resources