CATiledLayer and UIScrollView, caching old data for some zoom levels - iphone

I have a CATiledLayer set up to dynamically re-draw some graphics I'm creating with Quartz.
This view is added as the content view of a scroll view, and as I scroll and zoom the graphic is redrawing at the proper resolution and doing exactly as I expected.
My problem is that If I change the values that generate the graphic (it is a visualization of a graph like structure), then setNeedsDisplay on the view, the changes will apply and be shown at some zoom levels, but if you zoom out it will show the old graphic that was generated. Zooming in again goes back to the proper graphic.
I assume that the different zoom levels in the tiled layer are being cached, and so the update isn't being applied to the already cached zoom levels.
My question is, if this is the case, is there any way to force the CATiledLayer to redraw not from the cached data? And if not, does anyone know what could be causing the problem?

Not sure if you are still having this issue but I have run into it myself recently. This seems like the same issue from Clear CATiledLayers Cache When Changing Images The answer seems to be to set the layer.content to nil and then call for a refresh with setNeedsDisplay or setNeedsDisplayInRect:. So wherever you would have called setNeedsDisplay to refresh the view after wanting it cleared you would instead use:
layer.content = nil;
[layer setNeedsDisplay];
This seems to work for me though in the comments for the answer of the above linked question there are warnings against directly setting the layer.content property. Aside from possibly removing and re-adding the layer though I have not been able to come up with any other options.

Related

Synchronizing EAGLView presentation with frame resize on iPhone

I have an EAGLView which I am resizing in an animation, with the animation driven by an NSTimer that calls a draw function. As I resize the EAGLView, I need to adjust the projection for the view to maintain the aspect ratio of the contents. The draw function looks like this:
gameView.frame = newFrame;
[gameView setFramebuffer];
[self updateProjection];
/* Drawing to the EAGLView, gameView, here, then... */
[gameView presentFramebuffer];
Run like this, though, the contents of the EAGLView appear to "stair-step" down the animation. When I record it and look at it frame by frame, it's clear that the projection is adjusted, then the view is resized a very short time later (less than one animation frame).
I suspect that the reason is that the change in the frame of gameView is being deferred, and that in the meantime the updated framebuffer is making its way to the screen. I've tried using CATransactions to get the frame update to take effect immediately, but as I kind of expected for a UIView change, that did nothing. I suppose I could modify the viewport and leave the EAGLView full-frame, but I worry that that might just leave me with synchronization issues elsewhere (say, with updates to any overlaid CALayers).
Does this seem like a reasonable assessment of the problem? How can I prevent it -- that is, how can I best ensure that the framebuffer presentation coincides with the change in the actual frame of the EAGLView (and other CA elements)?
Thanks!
I encountered a similar issue with a custom EAGLView I wrote. The problem was that when changing orientation, the view got resized and the contents stretched, and only after the animation, the scene with the right proportions was displayed.
I think yours is the same problem, and I don't think you can force CoreAnimation to wait for your view to update before rendering an animation frame, because of the way it works (CA isn't aware of the drawing mechanism of the view or the layer on which operates).
But you can easily bypass the problem: open you xib file containing the view, switch to the attributes inspector on the right. Select your EAGLView (the same applies to GLKView), and under the section View there's the Mode attribute. If you're creating the view programmatically, set the contentModeproperty.
This value describes how the view contents are managed during an animation. Until the buffer is displayed, the old scene gets resized according to that mode; perhaps you can find a mode that fits the animation you need to achieve.
If you're worried about proportions, the center content mode may work, but the part of scene that hasn't been rendered yet will appear empty during the animation.
Unfortunately the redraw mode won't necessarely make your EAGLView refresh: the view is actually redrawn, but potentially with the old contents of the color buffer. It's a problem of getting the buffer filled at the right time.
You could try to resize and prerender a frame big enough to cover the whole animation before starting it; or you could try to render a frame to an UIImage, replace the view on the fly, animate it, the place the EAGLView back, but this may seriously affect performance.
The way I solved my issue was making simply the EAGLView big enough.
Regarding the blending of other CALayers, as far as I tried, I encountered no synchronization issue; the EAGLView updates when it can, and the other layers too. Just set the right properties to CAEAGLLayer if you're using alpha channel, and rememeber that blending the layers every time OpenGL updates your scene may be expensive in terms of performance.

Maintaining proportions when autorotating custom UIView

This is probably either real easy, real dumb, or my google fu has taken a serious turn for the worse. Anyway, I'm implementing custom view for my app, which is using pure CGContext drawing, no subviews (for now at least). The thing is, I want it to autorotate, so I have shouldAutorotateToInterfaceOrientation return YES, and voila, the view rotates. But in doing so it's not actually redrawing the content (which I assume is rendered into a texture somewhere in the framework and splashed onto a rect, but that's not really relevant here), the rect is simply stretched, squishing the content. How can I get it to simply issue a draw of a bigger area while rotating? That is, my content is bigger than the screen, and I'd simply like the viewport to change during the rotation.
I've tried setting the view's contentMode to UIViewContentModeRedraw, but that didn't do anything, I've tried playing around with the autoresizeMask stuff, but didn't seem to help either. I've also tried inserting a setNeedsDisplay in willRotateToInterfaceOrientation, however that only caused it to redraw using the new bounds (i.e. squishing it first, and then stretching it out to the right size during the rotation), which is also not what I'd like to see.
Does anyone have any idea how I might go about getting this to work?
As it turns out, it's a mix of dumb and easy. I'm posting it here if anyone should care to read it someday. They way I managed to solve it was actually sandwiching a view between window and my view (I suppose you might be able to go to work on the window directly, but it felt more intuitive this way). That is, I added my view as a child view to that view, which I'll call the frame.
The frame is a resizing as normal, however, I turn OFF resizing of child views, and make my own view LARGER than the viewing area (square actually, 480x480, so it can cover the entire screen either way). Problem solved, basically.
Now I'm playing around with animating the offset of the view in the frame during willRotateToInterfaceOrientation, to have it appear to be rotating around the center, rather than the upper left corner, but that's a different question.

How can I stop the iPhone from displaying a transform change until after the screen is redrawn

I have found the UIScrollView's zooming mechanism to be clunk and essentially unusable. So instead, I'm rolling my own. I have a UIView that resizes itself with the pinch-zoom, and that's working fine. When the zoom is complete, the view needs to reset its transform and redraw the images.
The zoom works essentially in the same way the UIScrollView does. It sets the transform property of the UIView until complete. Then, when the zoom finishes, I want to reset the transform to CGAffineTransformIdentity, resize the frame to be the size it was before, and tell the view to redraw itself at the new size.
It all works pretty well, except when I change the transform to identity then redraw the image, there is a slight flicker before the image completely redraws. This is due to the fact that I'm using a subclass of CATiledLayer, since the view can be of arbitrary size.
I've overridden the fadeDuration to be zero, but there is still a flicker while the transform is reset before the redraw is finished. Is there any simple way to overcome this without creating another view to draw with then replacing it?
Problem solved using a lower-level approach to layers.

CATiledLayer blanking tiles before drawing contents

All,
I'm having trouble getting behavior that I want from CATiledLayer. Is there a way that I can trigger the tiles to redraw without having the side-effect that their areas are cleared to white first? I've already subclassed CATiledLayer to set fadeDuration to return 0.
To be more specific, here are the details of what I'm seeing and what I'm trying to achieve:
I have a UIScrollView with a big content size...~12000x800. Its content view is a UIView backed by a CATiledLayer.
The UIView is rendered with a lot of custom-drawn lines
Everything works fine, but the contents of the UIView sometimes change. When that happens, I'd like to redraw the tiles as seamlessly as possible. When I use setNeedsDisplay on the view, the tiles redraw but they are first cleared to white and there's a fraction-of-a-second delay before the new content is drawn. I've already subclassed CATiledLayer so that fadeDuration is set to 0.
The behavior that I want seems like it should be possible...when you zoom in on the scrollview and the content gets redrawn at a higher resolution, there's no blanking before the redraw; the new content is drawn right on top of the old one. That's what I'm looking for.
Thanks; I appreciate your ideas.
Update:
Just to follow up - I realized that the tiles weren't being cleared to white before the redraw, they're being taken out entirely; the white that I was seeing is the color of the view that's beneath my CATiledLayer-backed view.
As a quick hack/fix, I put a UIImageView beneath the UIScrollView, and before triggering a redraw of the CATiledLayer-backed view I render its visible section into the UIImageView and let it show. This smooths out the redraw significantly.
If anyone has a better solution, like keeping the redraw-targeted tiles from going away before being redrawn in the first place, I'd still love to hear it.
I've found that if you set levelsOfDetailBias and levelsOfDetail both to the same value (2 in my case), then it only redraws the tiles that are touched by my setNeedsDisplayInRect: call, as you'd hope.
However if the levelsOfDetail is different to LODB, then any calls to setNeedsDisplayInRect: redraw all the tiles.
You could add another layer (possibly a CATiledLayer) behind the existing tiled layer. (Sort of a double-buffered solution.) You would call setNeedsDisplay: on the second layer from a timer that fires after a few seconds to ensure that that layer doesn't redraw at the same time as the front layer.
Another potential option is to use the same delegate to draw content to a bitmap context and swap the bitmap into the backing store once the content is refreshed. This should produce a flicker-free result. That being said, I can't tell you how this might be done, and one nice thing about CATiledLayers is they automatically generate tiles when you zoom and pregenerate tiles when you pan once zoomed in.
I would like to see how you implement your application. I have been looking for weeks to find an example that uses a combination of UIScrollView and a CATiledLayer-back view with a lot of custom drawn lines. Apple has some great sample code - but it all involves images rather than line art, so no help for me.
Having read through these answers without a solution, I discovered that tiling a page was the dominant background task.
Preparing my lo-res placeholder image on a high priority queue solved this issue - the images now appear while the tiling is occurring. Caching the placeholder images further improves their appearance - they appear before the tiling begins.
With newer devices, the tiling it so fast, these tricks might not matter. A sample PDF consisting of large scanned images (e.g. a scanned book) tiles the slowest in my experience, and makes for good test data.
I had the same problem with iPad.
The solution was more simple than I thought and far more simple than using UIImageView to render display before redrawing... :
Just don't set any background color for the Layer!
I had CATiledLayer set in a similar way:
layer = [[CATiledLayer alloc] init];
layer.masksToBounds = YES;
layer.contentsGravity = kCAGravityLeft;
//layer.backgroundColor = [[UIColor whiteColor] CGColor];
layer.tileSize = CGSizeMake(1004.0, 1004.0);
layer.levelsOfDetail = 16;
layer.levelsOfDetailBias = 8;
Note that I have commented out the line setting layer's background color to white.
After that the white blank before redraw problem disappeared!
Let me know if anyone has tried that.

How to prevent UIView's drawRect from clearing out the paths drawn by previous drawRect calls?

Everytime UIView's drawRect is called, the content drawn by previous drawRect (using Core Graphics) is cleared out. How can I make it so that the paths rendered from previous drawRect calls stay until I explicitly clear it?
You basically need to clip the 'dirty' part of your rect where changes have been made, and only this part will be re-drawn.
- (void)setNeedsDisplayInRect:(CGRect)invalidRect
I'm fighting this issue myself right now. The problem is there is a property on the UIView called "clearsContextBeforeDrawing" that according to the documentation is supposed to fix this problem, however it doesn't work that way in my experience.
I think ultimately the solution to this is going to be to allocate an offscreen buffer and do all my drawing there, then blt it over to the UIView in the drawRect method.
I'm pretty certain there's no way around this; it's how UIView is designed to work. If asked to, your custom view should be able to draw any part of itself at any time. This is partly because views can do more than just appear onscreen. e.g. on the desktop they can be printed, and even on the iPhone, you might wish to capture the contents of a view to a bitmap.