CATiledLayer being removed and refreshed while zoomed in iPad 3rd gen - objective-c

I'm experiencing a redraw problem on a CATiledLayer when the parent UIScrollView is zoomed in.
I'm rendering a PDF page in a CATiledLayer backed UIView. It has another UIImageView behind it, which contains a low-res image of the page that the CATiledLayer will draw. When I zoom in, it works as expected. The CATiledLayer will render a higher resolution image according to the zoom level.
The problem occurs after zooming. If I zoom in then just leave the iPad alone, the displayed image blurs then resharpens. It looks like the CATiledLayer is being removed, since I see the blurry low resolution image in the backing view, then the CATiledLayer gets redrawn, i.e. I see the tiling effect and the resharpening of the image. This happens if I just leave the app alone and wait about 30 to 40 seconds. I've only observed it on the iPad 3rd gen (New iPad, iPad3, whatever). I'm also testing on iPad2s and I have yet to encounter the issue.
Has anyone else encountered this problem? Any known cause and, possibly, solutions?
Edit:
My UIScrollViewDelegate methods are as follows:
// currentPage, previousPage, and nextPage are the pdf page views
// that are having the refresh problem
- (void)positionBufferedPages {
// performs math {code omitted}
// then sets the center of the views
[previousPage.view setCenter:...];
[nextPage.view setCenter:...];
}
- (void)hideBufferedPages {
if ([previousPage.view isDescendantOfView:scrollView]) {
[previousPage.view removeFromSuperview];
}
if ([nextPage.view isDescendantOfView:scrollView]) {
[nextPage.view removeFromSuperview];
}
}
- (void)showBufferedPages {
if (![previousPage.view isDescendantOfView:scrollView]) {
[scrollView addSubview:previousPage.view];
}
if (![nextPage.view isDescendantOfView:scrollView]) {
[scrollView addSubview:nextPage.view];
}
if (![currentPage.view isDescendantOfView:scrollView]) {
[scrollView addSubview:currentPage.view];
}
}
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
return currentPage.view;
}
- (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view {
[self hideBufferedPages];
}
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollViewParam withView:(UIView *)view atScale:(float)scale {
[self positionBufferedPages];
[self showBufferedPages];
}
- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
// nothing relating to the pdf page view
// but does set the center of some other subviews on top of the pdf page views
}
Not sure how helpful this will be though, as the scrollview is not receiving input while the problem happens. As mentioned above, the pdf page is loaded to the CATiledLayer, then I leave the iPad alone (no input is received by the device), and the CATiledLayer will redraw by itself.
I've also tried catching calls to setNeedsDisplay, setNeedsDisplayInRect:, setNeedsLayout, and setNeedsDisplayOnBoundsChange: on both the view and the tiled layer, but the redraw happens without any of those functions getting called. drawLayer:inContext: gets called, of course, but the trace only shows some Quartz calls being started in a background thread (as expected, as tiled layers prepare the content in the background), so it is of no help either.
Thanks in advance!

How is your app's memory usage looking? CATiledLayer will discard its cache and redraw if a memory warning occurs. I've seen it do this even without memory warnings being sent to the app (just a higher than usual memory load). Use Instruments to see memory usage. You may need to use the OpenGL ES Driver instrument to see what's going on with graphics memory.

I spoke with an Apple engineer about this and the short answer is that iOS only has X amount of memory available for caching a CATiledLayer and on the Retina display of the iPad, there are just too many pixels to use more than one layer.
I had been using two CATileLayers to display a map view and a drawing view on top. I removed the second CATiledLayer and the problem went away.

I've had the exact same problem. In my case it was caused by using the UIGraphicsBeginImageContext() function; this function does not take scale into account, which gives problems on a retina display. The solution was to replace the call with UIGraphicsBeginImageContextWithOptions(), with the scale (=third) parameter set to 0.0.
If you based your code on the Zooming PDF Viewer sample code from Apple, like I did, chances are this will solve your problem too.

Longshot, any chance you are calling any methods on the views from the non-main thread? All sorts of unexpected funky stuff can happen if you do.

Related

Terminated due to Memory Error when load number of images into UIScrollView

I am using UIScrollView in my app that loads number of images(more then 200 images).
I load images from disk and just add into the UIScrollView.I get the Xcode 5 error message "Terminated due to memory error" and app has Terminated unexpectedly.But not every time it has happened occasionally.
I am still entirely not sure this is a memory problem.But i didn't found the code cause of the Memory Problem.
Even I have checked memory leak issue through the instrument tool there is no memory leak.
I have created custom reusable UIView Class with help of Custom ACReuseQueue for UIScrollView like UITableView due to the efficiency and time consuming of the object creation
I have implemented enqueue and dequeue concept inside the scrollViewDidScroll:
To add image into the UIScrollView: used to following code
-(void)imageAdd:(ALAsset *)item
{
__block ImageControl *imageControl;
imageControl = (ImageControl*)[[ACReuseQueue defaultQueue] dequeueReusableObjectWithIdentifier:#"ImageControl"];
[imageControl setAlAsset:item];
[imageControl setDuration:1.0];
[imageControl setIsCopy:NO];
[self.galleryView addItem:imageControl];
[imageControl release];
}
below code is used to when user scroll the ScrollView
-(void)scrollViewDidScroll:(UIScrollView*)scrollView
{
for (ImageControl *imageControl in self.galleryView.items)
{
if (CGRectIntersectsRect(imageControl.frame, self.galleryView.bounds))
{
imageControl = (ImageControl*)[[ACReuseQueue defaultQueue] dequeueReusableObjectWithIdentifier:#"ImageControl"];
[self.galleryView addItem:imageControl];
}
else
{
[imageControl removeFromSuperview];
[[ACReuseQueue defaultQueue] enqueueReusableObject:imageControl];
}
}
}
Still,i didn't achieve the Reusable UIScrollView like UITableView
Don't load all of the images at the same time. Most aren't visible anyway.
Using a table view will likely make your life a lot easier and be the fastest way of having only the required images loaded.
If you want to use the plain scroll view, you need to implement the scrollViewDidScroll: delegate method and use it to add and remove subviews as the user scrolls the view.

Cocoa Application does not redraw after overlapping another application

After I overlay an application over my previous application, I go back to previous application and encounter a few errors:
certain components have disappeared
only way to make the components visible is to resize the window
that seems to redraw the whole canvas.
Weird thing is that there are only a couple of components and drawn images that are missing
It doesn't always happen but only a couple of times
I haven't found a solid way to reproduce the problem.
Anybody have an Idea why this is happening?
I experienced exactly the same issue (view was updated correctly only after resizing), except that I've used OpenGL drawing in OSX game.
My problem was solved by adding this:
GLint vblSynch = 1;
[[self openGLContext] setValues:&vblSynch forParameter:NSOpenGLCPSwapInterval];
in my custom NSOpenGLView init method.
Then I've implemented:
- (void)drawRect:(NSRect)rect
{
[self destroyFramebuffer]; // glDeleteFramebuffers..
[self createFramebuffer]; // [super prepareOpenGl], glGenFrame(Render)Buffers, bind buffers, etc
[self drawView]; // [[self openGLContext] makeCurrentContext], make some drawing, [[self openGLContext] flushBuffer]..
}
like this.
After these changes, when window gets focus it redraws itself (without any resizing stuff :) ).
Hope this helps!

Zoom with a drawing view

I'm looking for a solution in order to have a beautiful zoom on a drawing view.
In my app, I have a view with an other UIView (which is used like a drawing view) and when I draw a stroke on it, the stroke is perfect. But when I zoom the view, I have this really ugly effect (a pixelised stroke) :
(source: imagup.com)
url image
Is there a solution in order to have a proper stroke ?
My UIViewController has a hierarchy like that :
UIViewController
ScrollView
View zoomable (defined with the viewForZoomingInScrollView method)
Image view
Drawing view
Thanks a lot !
Regards,
Sébastien ;)
I'm in the process of making a vector drawing application and let me tell you, this is NOT a trivial task to do correctly and requires quite a bit of work.
Some issues to keep in mind:
If you are not using vector graphics (CGPaths, for example, are
vectors) you will NOT be able to remove the pixelation. A UIImage,
for example, only has so much resolution.
In order to get your drawing to not look pixelated, you are going to
have to redraw everything. If you have a lot of drawing, this can be
an expensive task to perform.
Having good resolution WHILE zooming is nearly impossible because it would require an excessively large context and your drawing would likely exceed the capabilities of the device
I use core graphics to do my drawing, so the way I solved this issue was by allocating and managing multiple CGContexts and using them as buffers. I have one context that is ALWAYS kept at my least zoomed level (scale factor of 1). That context is drawn into at all times and makes it so that when unzooming completely, no time is spent redrawing since it is already done. Another context is used soley for drawing when zoomed. When not zoomed, that context is ignored (since it will have to be redrawn based on the new zoom level anyway). A high level algorithm for how I perform my zooming is as follows:
- (IBAction)handlePinchGesture:(UIGestureRecognizer *)sender
{
if(sender.state == UIGestureRecognizerStateBegan)
{
//draw an image from the unzoomedContext into my current view
//set the scale transformation of my current view to be equal to "currentZoom", a property of the view that keeps track of the actual zoom level
}
else if(sender.state == UIGestureRecognizerStateChanged)
{
//determine the new zoom level and transform the current view, keeping track in the currentZoom property
//zooming will be pixelated.
}
else if(sender.state == UIGestureRecognizerStateEnded || sender.state == UIGestureRecognizerStateCancelled)
{
if(currentZoom == 1.0)
{
//you are done because the unzoomedContext image is already drawn into the view!
}
else
{
//you are zoomed in and will have to do special drawing
//perform drawing into your zoomedContext
//scale the zoomedContext
//set the scale of your current view to be equal to 1.0
//draw the zoomedContext into the current view. It will not be pixelated!
//any drawing done while zoomed needs to be "scaled" based on your current zoom and translation amounts and drawn into both contexts
}
}
}
This gets even more complicated for me because I have additional buffers for the buffers because drawing images of my paths is much faster than drawing paths when there is lots of drawing.
Between managing multiple contexts, tweaking your code to draw efficiently into multiple contexts, following proper OOD, scaling new drawing based on your current zoom and translation, etc, this is a mountain of a task. Hopefully this either motivates you and puts you on the right track, or you decide that getting rid of that pixelation isn't worth the effort :)
I had the same problem and found a solution: tell the view to use a CATiledLayer as backing layer, then tell the view how many levels of zoom it supports. This worked for me, my drawing methods get automatically called when the (parent) view is zoomed.
A short explanation of levelsOfDetail and levelsOfDetailBias:
levelsOfDetail determine how many zooming levels there are in total
levelsOfDetailBias determine how many of those are zooming in.
So in my example I have 4 zooming levels, 3 are zoomed in and 1 is the non-zoomed level, meaning my view only redraws when zooming in.
#imprementation MyZoomableView
+ (Class)layerClass
{
return [CATiledLayer class];
}
- (id)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame])) {
((CATiledLayer *)self.layer).levelsOfDetail = 4;
((CATiledLayer *)self.layer).levelsOfDetailBias = 3;
}
return self;
}
#end
Use [self setContentScaleFactor:scale]; in your scrollViewDidEndZooming: delegate method.

Possible to manually rotate UIPopoverController's view?

The view system in my app is highly customized and uses a number of views that are manually rotated from portrait to landscape based on user interactions (the rotation is done by applying an affine transform to the view/layer).
I want to present a popover inside one of these rotated views, but the orientation of the popover always appears relative to the orientation of the device (i.e., not relative to the view). I'm guessing the answer is no, but just in case someone has a clever idea: is there any way to manually rotate the view that is presented by UIPopoverController?
Sean, I just tested it for kicks, yes it works.
It has to be done (in my case at least) in viewDidAppear (if done in viewWillAppear, it gets knocked back to the original setting.)
This worked just fine (just tested now) to have a popover at a 90 degree angle. i.e in my case my main view is in portrait mode and the popover is turned 90 deg.
self.navigationController.view.superview.superview.transform = CGAffineTransformMakeRotation (M_PI/2.0);
Are you trying to rotate the popover or just the content shown in the popover? You can control some of the former by setting which arrow orientations are possible. I'm interested in the latter, and it seems to work just by grabbing the content view controller. E.g.:
aPopoverController.contentViewController.view.transform = CGAffineTransformMakeRotation(M_PI);
DISCLAIMER: If you're at all interested in trying to get your app into the store, this code is almost certainly grounds for rejection. It dives into UIKit's private API's which is a big no-no as far as apple is concerned.
#RunningPink had the right idea. Depending on how the view hierarchy is set up, the popover may be back up farther than two superviews. The popover itself it an instance of the (private) class _UIPopover (at least in iOS 5). You can find this view by doing:
UIView *possiblePopover = popoverController.contentViewController.view;
while (possiblePopover != nil) {
// Climb up the view hierarchy
possiblePopover = possiblePopover.superview;
if ( [NSStringFromClass([possiblePopover class]) isEqualToString:#"_UIPopoverView"] ) {
// We found the popover, break out of the loop
break;
}
}
if (nil != possiblePopover) {
// Do whatever you want with the popover
}
In doing this, I found that transforming the view often ended up making the popover look blurry. I found the reason was that the popover's superview was an instance of another private class called UIDimmingView which is responsible for accepting touches outside of the popover and causing the popover to dismiss. Performing the rotation on the dimming view removed the blurriness I was seeing in the popover.
However, transforming the dimming can result in weirdness where certain parts of the window are not "covered" by the dimming view so the popover will not dismiss if these parts of the window are tapped. To get around this, I applied the rotation to the dimming view, reset the dimming view's frame to cover the screen, and then translated the popover view into place.
if (nil != possiblePopover) {
// Found the popover view
CGAffineTransform rotation = CGAffineTransformMakeRotation(-M_PI_2);
CGAffineTransform translation = // Whatever translation in necessary here
// Rotate the UIDimming View and reset its frame
[possiblePopover.superview setTransform:rotation];
[possiblePopover.superview setFrame:CGRectMake(0, 0, possiblePopover.superview.frame.size.height, possiblePopover.superview.frame.size.width)];
// Translate the popover view
[possiblePopover setTransform:translation];
}

Using NSImageView to display multiple images in quick sucession

I have an application where, in one window, there is an NSImageView. The user should be able to drag and drop ANY FILE/FOLDER (not only images) into the image view, so I subclassed NSImageView class to add support for those types.
The reason why I chose an NSImageView instead of a normal view is because I also wanted to display an animation (say an arrow pointing downwards and going up and down) when the user hovers over with files ready to drop. My question is this: what would be the best way (most efficient, quickest, least CPU usage, etc) to do this?
In fact, I have already done it, but what made me ask this question is the fact that when I set the images to change at a rate below 0.02 sec it starts to lag. Here is how I did it:
In the NSImageView subclass:
have an ivar: NSTimer* animTimer;
override awakeFromNib, calling [super awakeFromNib] and loading the images into an array (about 45 images) using NSImage
whenever user enters with files, start animTimer with frequency = 0.025 (less and it lags), and a selector that sets the next image in the array (called drawNextImage)
whenever the user exits or ends the drag and drop, call [animTimer invalidate] to stop updating images
Here is how I set the image in the subclass:
- (void)drawNextImage
{
currentImageIndex++; // ivar / kNumberDNDImages is a constant defined as 46
if (currentImageIndex >= kNumberDNDImages) { currentImageIndex = 0;}
[super setImage: [imagesArray objectAtIndex: currentImageIndex]]; // imagesArray is ivar
}
So, how would I do this quick enough? I'd like the frequency to be about 0.01 secs, but less than 0.025 lags, so that is what I have set for the moment. Oh, and my images are the correct size (+ or - one pixel or something) and they are in .png (I need the transparency - jpegs, for example, won't do it).
EDIT:
I have tried to follow NSResponder's suggestion, and have updated my method to this:
- (void)drawNextImage
{
currentImageIndex++;
if (currentImageIndex >= kNumberDNDImages) { currentImageIndex = 0;}
NSRect smallImgRect;
smallImgRect.origin = NSMakePoint(kSmallImageWidth * currentImageIndex, [self.bigDNDImage size].height); // Up left corner - ??
smallImgRect.size = NSMakeSize(kSmallImageWidth, [self.bigDNDImage size].height);
// Bottom left corner - ??
NSPoint imgPoint = NSMakePoint(([self bounds].size.width - kSmallImageWidth) / 2, 0);
[bigDNDImage drawAtPoint: imgPoint fromRect: smallImgRect operation: NSCompositeCopy fraction: 1];
}
I have also moved this method and the other drag and drop methods from the NSImageView subclass to an NSView subclass I already had. Everything is exactly the same, except for the superclass and this method. I also modified some constants.
In my early testing of this, I got some error/warning messages that didn't stop execution talking abou NSGraphicsContext or something. These have disappeared now, but just so you know. I have absolutely no idea why they were appearing and what they mean. If they ever appear again I'll worry about them, not now :)
EDIT 2:
This is what I'm doing now:
- (void)drawNextImage
{
currentImageIndex++;
if (currentImageIndex >= kNumberDNDImages) { currentImageIndex = 0;}
[self drawCurrentImage];
}
- (void)drawCurrentImage
{
NSRect smallImgRect;
smallImgRect.origin = NSMakePoint(kSmallImageWidth * currentImageIndex, 0); // Bottom left, for sure
smallImgRect.size = NSMakeSize(kSmallImageWidth, [self.bigDNDImage size].height);
// Bottom left as well
NSPoint imgPoint = NSMakePoint(([self bounds].size.width - kSmallImageWidth) / 2, 0);
[bigDNDImage drawAtPoint: imgPoint fromRect: smallImgRect operation: NSCompositeCopy fraction: 1];
}
And the catch here is to call drawCurrentImage when drawRect is called (see, it actually was easier to solve than I thought).
Now, I must say I haven't tried this with the composite image, because I couldn't find a good and quick way to merge 40+ images the way I wanted (one next to the other). But for the ones ineterested, I modified this to do the same thing as my NSImageView subclass (reading 40+ images from an array and displaying them) and I found no speed bump: NSView is as laggy below 0.025 as NSImageView. Also I found some problems when using core animation (the image is drawn in weird places instead of the place I tell her to) and some warnings talking about NSGraphicsContext, which I don't know how to solve at all (I'm a complete noob when it comes to drawing and such with Objective-C's tools). So for the time being I'm using NSImageView, unless I find a way to merge all those images and try it with NSView.
Core Animation would probably be quickest, since it'll do everything on the GPU. Create a layer for each image, setting each layer's contents to the CGImage you can make from each image, add them all as sublayers of a single top-level layer, host the top-level layer in a plain NSView, and then just toggle each image layer's hidden property in turn.
I'd probably draw all of the component images into one long image, and draw segments into a view using -drawAtPoint:fromRect:operation:fraction:. I'm sure you could make it faster than that by resorting to OpenGL, though.