CATiledLayer PDF Performance is Poor on iPad 3 Retina Display - ios

I'm using rather straightforward code to display a zoomable PDF in a scrollview, and it has been working beautifully on the iPad 2 and the original iPad. But it's staggeringly slow on the iPad 3. I know I'm pushing more pixels, but the rendering performance is simply unacceptable.
In iOS 5.0 and later, the tileSize property is arbitrarily clamped at 1024, which means tiles appear half that size on the retina display. Has anyone found a way to overcome this limitation?
Otherwise, has anyone found a way to improve the speed of the CATiledLayer on the iPad 3?

Have you tried setting shouldRasterize to YES on the layer?

Did you run a time profiler on these draws and did you rule out the possibility of redundant draws?
I've had some weird double drawing, which was easily found using:
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)context
{
NSLog(#"draw %#", NSStringFromCGRect(CGContextGetClipBoundingBox(context)));
// draw pdf
}
There's also a variety of settings to play with:
tiledLayer.levelsOfDetail = 2
tiledLayer.levelsOfDetailBias = 4
tiledLayer.tileSize = self.bounds.size
CGContextSetInterpolationQuality(context, kCGInterpolationLow)
CGContextSetRenderingIntent(context, kCGRenderingIntentDefault)
self.contentScaleFactor = 1.0

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
);

'Display Zoom' iPhone 6/6s setting blurs graphics

I'm writing a project in Xcode 7 / Swift 2 that it is optimized for iPhone 6/6s (i.e. the project has a launch screen file and launch screen images for iPhone 6/6s).
Fortunately or unfortunately, iPhone 6 users have the ability to turn on the ‘Display Zoom’ setting on device which enlarges elements of the interface. When turned on, this setting effectively enlarges a standard iPhone 5 screen size to fit in the iPhone 6 screen space, upsampling to x1.171875. This upsampling causes elements that are raster based such as images, icons, or views that contain UIBezierPath() drawings to display blurred (mildly but noticable).
A few questions:
Appreciate any experienced responses on this conundrum. Thanks.
1 - How can I instruct elements (e.g. a UIView) on the Storyboard in code to disregard the Display Zoom setting when the user has turned it on?
2 - What techniques are there to ensure pixel perfect accuracy remains when Display Zoom is on? (e.g. Is it possible to render graphics using OpenGL rendering, if so, how?)
3 - Is it possible to replace a x2 image with a x4 image to reduce any blurring when Display Zoom is on? (i.e. will iOS downsample a x4 image to x2 image on iPhone 6?)
4 - How can UIBezierPath() drawings maintain pixel perfect accuracy when Display Zoom is on?
There's nothing you can do about this. A user who chooses zoomed mode is deliberately throwing away pixel accuracy. The points in the drawing no longer match the pixels on the screen one-to-one (or one-to-two or one-to-three or any integral ratio). This choice therefore blurs the screen for everything the user does, not just your app.
Nor can you detect what is happening, because in effect zoomed iPhone 6 is presented to your app as an iPhone 5 (and a zoomed 6 Plus is presented to your app as a 6).
As #matt says, there's nothing you can do about this for normal UIKit content
However, for Open GL ES or Metal content, you are able to opt-out of the sampling that the device does, and render straight into the device's physical coordinates - allowing for pixel perfect drawing.
In a graphics app that uses Metal or OpenGL ES, content can be easily rendered at the precise dimensions of the display without requiring an additional sampling stage. This is critical in high-performance 3D apps that perform many calculations for each rendered pixel. Instead, create buffers to render into that are the exact resolution of the display.
Open GL ES
Set the contentsScale of the CAEAGLayer to the [UIScreen mainScreen].bounds.nativeScale, or use a GLKView which will automatically do this.
You will then want to create your framebuffer with the size of the device's physical coordinates.
Metal
Set the contentsScale of your CAMetalLayer to the [UIScreen mainScreen].bounds.nativeScale, or use an MTKView which will automatically do this.
You will also want to adjust the drawable size to account for the scale (lifted from the docs):
CGSize drawableSize = self.bounds.size;
drawableSize.width *= self.contentScaleFactor;
drawableSize.height *= self.contentScaleFactor;
metalLayer.drawableSize = drawableSize;
See also this interesting blog post on how the iPhone 6 Plus renders content, plus the follow-up post specifically about Display Zoom.

iOS CATiledLayer and TilingView scale problems?

I am using the TilingView from the Apple PhotoScroller example to tile some images. This works great for most of my images, but I have a few get weird scale values. I set the level of detail to 4. My images are all scaled at different values, 100,50,25,12.5 scales then tiled 256x256 at those levels.
In TilingView drawRect method, the scale I get here must be one of 4 values and normally is 1.0,0.50,0.25,0.125. Since I store my images off based on these scale values when I get a weird scale value it breaks and cannot load the images. For example I have an image that at .50 scale the actual value I get is 0.499798.
Any ideas whats going on here? If I tell the CATiledLayer to have 4 levels of detail, how do I end up with these weird values?
CGFloat scale = CGContextGetCTM(context).a;
NSLog(#"scale = %f",scale);
CATiledLayer *tiledLayer = (CATiledLayer *)[self layer];
CGSize tileSize = tiledLayer.tileSize;
How can I ensure that the image size I pass actually will return me one of the 4 scales 100,50,25,12,5 for any image size I specify?
There are several bugs in that sample's code, one of which involves proper rounding of those scale values, which leads to the issue you are seeing. But there are also other subtle issues. Please have a look at this question, where those issues (and the fixes) are described in more detail.

iOS GLKit texture blurry on retina display

I am working with OpenGL ES 2.0 and GLKit for iOS.
My app only needs to run at a resolution of 480 by 320 just like the pre iPhone 4 displays as it is using retro style graphics.
The texture graphics are made according to this resolution and a GLKit projection matrix of (0, 480, 0, 320).
This all looks fine on the 3GS but on later models OpenGL (understandably) does some sort of resizing in order to stretch the scene. This resizing results in an undesirable blurring/smoothing of the graphics - probably using some sort of default interpolation scheme.
Is it possible to affect the way this resizing is done by OpenGL? Preferably setting it to no interpolation where the pixels are just directly enlarged.
You need to set the scaling filters on the view like this.
self.layer.magnificationFilter = kCAFilterNearest;
self.layer.minificationFilter = kCAFilterNearest;

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.

Resources