I would like to have a UIView subclass that implements a method similar to setNeedsDisplay, except that redrawing (i.e., that would usually be called via drawRect:) will occur in a background thread sometime soonish, rather than at the end of the current update cycle.
It might be called setNeedsAsynchronousDisplay. Or the existing setNeedsDisplay could get hijacked and not cause redraw at the end of the cycle, or whatever, as long as it lets the redraw not happen on the main thread blocking screen updating an interaction until its completed.
Until the redraw occurs, the view can continue to use its current drawn representation.
Is something along these lines reasonably doable?
Thanks!
Yes it is possible. You will probably need to generate the content view as an image in the background and push the images into a nsdictionary or array.
So while your background is generating the images you can just show the image in drawrect function by rendering the image, providing the image has been generated.
A WWDC video that shows how to do it: WWDC 2012 session 211 - Building Concurrent User Interfaces on IOS. Here is the video description:
For a great user experience, it's essential to keep your application responsive while it renders complex UI elements and processes data. Learn how to use concurrency at the UIKit layer to perform drawing and other common operations without blocking user interaction.
No. View Drawing must occur in the foreground. Apple makes that very clear in their docs.
EDIT: You're right that you CAN do Core Graphics drawing in the background as long as it's not in a UIView object's drawing methods. You'd have to do the drawing in the background, then send a message to the main thread to update your view object once drawing is complete.
I would suggest not trying to override setNeedsDisplay. Instead, add your new setNeedsAsynchronousDisplay method. In that method, queue rendering code to an async queue using GCD calls. Once the rendering is complete, have the rendering code send a setNeedsDisplay message to self on the main thread.
Then in your subclass's drawRect method, check for pre rendered images and draw those into the view's context instead of the normal code.
A downside of this is that merely by implementing drawRect, you may slow down rendering because the system calls that instead of doing other, more efficient things to render your view's content.
Related
In my case, the drawRect: will not be called immediately after every single setNeedsDisplay is called. For example, I think the following code is same as my case.
for (int i = 0; i < 100; ++i)
{
[self setNeedsDisplay];
}
From the documentation:
When the actual content of your view changes, it is your responsibility to notify the system that your view needs to be redrawn. You do this by calling your view’s setNeedsDisplay or setNeedsDisplayInRect: method of the view. These methods let the system know that it should update the view during the next drawing cycle. Because it waits until the next drawing cycle to update the view, you can call these methods on multiple views to update them at the same time.
drawRect: will only be called in time for the next frame to be drawn, which means your entire loop will result in drawRect: only being called once at the next rendering iteration. This saves unnecessary computation as it avoids drawing frames that will never be displayed on the screen. It also lets you make multiple changes in separate places in your code, each time notifying the view that a refresh is needed, without losing performance, since calling setNeedsDisplay only tells the drawing system that a redraw is needed in the next frame; it doesn't force the immediate rendering of a frame that might never be displayed on the screen.
setNeedsDisplay only marks the view as needing to be displayed again. The actual drawing call is done in the next runloop iteration of the main thread, once. This allows the drawing system to do some optimizations and "combine" repeated calls to setNeedsDisplay.
I have the problem that I have some really big UIScrollView and tons of images loaded on it as user scrolls. Images are stored on the device, however I receive information from server what to display on particular part of UIScrollView. When user scrolls a bit I need to show images at new position as I cannot afford to draw whole UIScrollView with images at startup. For the background I had one relatively small image which I move throughout the View. But the problem is that on top of that background I should draw a lot of UIImage objects(about 300-400) which are not particulary bih however are separeted on layers(one image on top of other on top of other etc.). Blocking scrolling while drawing is NOT an option.
Now I'm trying to decide which approach will suite my best:
Add all needed images to UIView on background thread and then just add UIView to ScrollView on main thread(which hopefully wont take long). Here when scroll somewhere I will need to calculate and create new UIView with objects and position it next to existing and eventualy to remove first UIView with all objects and layers when user continues to scroll in some direction.
Combine all layers in image with CoreGraphics and present them as objects with already decided layers. In this way I can remove specific object(image) from scroll view. When user scrolls I just create new objects and add them to view as full objects, and can remove objects when user scrolls enough instead of removing whole view. The problem here is adding multiple objects to UIScrollView on main thread, however when they are combined they won't be more than 15-20 objects.
My biggest concerns are performance and threading. As I said I cannot block main thread(or let's say cannot do this for a time that user will notice) and cannot combine images at my graphics department as they have tons of variatons which are decided at runtime. That's why I'm thinking of a way to prepare data on background thread and just really fast add it on main thread instead of preparing and adding it on main thread(which will block UI).
Every help will be greatly appriciated!
Regards,
hris.to
Look at using CATiledLayer for a UIView backing. It is was designed for this.
I have a map that has one UIView in a UIScrollView and the UIView is sized to the full size of the entire map. The CATiledLayer handles when to draw each tile of the view.
Ok, so I'm writing here just to let you know how I fix my issue.
Main problem was that I was moving a background picture while scrolling(so I don't load an enormous file) and while doing that I was fetching information from server and try to draw on the same tiles which makes system crash with well known crash:
CALayer was muted while enumerated
I was depending on performSelector method along with #synchronized but it turns out that this is not effective and sometimes two threads(main UI thread and background thread) were trying to change same tiles on screen. So basically what I did is to change background fetching and drawing from:
[self performSelectorOnBackgroundThread]
to using concrete background thread, to which I store reference:
#property(nonatomic, strong) NSThread* backgroundThread;
And now each time I need to load new tiles or I need to move background, I'm cancelling this thread(and make sure it's cancelled) before start any other operation. Also there was a little problem when switching between view with this thread and other views, as my thread hangs, and I needed to set a timeout:
[_backgroundThread cancel];
int currentAttempts = 0;
while( [_backgroundThread isExecuting] )
{
if( currentAttempts == MAX_ATTEMPTS_TO_CANCEL_THREAD )
{
//make sure we dont hang and force background
_backgroundThread = nil;
return;
}
[_backgroundThread cancel];
currentAttempts++;
}
In my 'scrollViewDidScroll' however, I didn't use the recursion as this results in slight UI blocks while scrolling on older devices(such as iPhone 4) which is unacceptable. So there I basically just cancel the thread and hope to get cancelled quick enough(which with dozens of tests appears to be working):
- (void)scrollViewDidScroll:(UIScrollView*)scrollView
{
[_backgroundThread cancel];
//move background around
[self moveBackground];
}
Downside of this approach is that you need lots of check in your background thread as calling 'cancel' to it won't actually cancel anything. As per apple docs it'll only change isCancelled state of your thread and you are responsible to make this thread quit in basically the same way as it'll quit normally(so the system has a chance to cleanup after your thread):
if( [_backgroundThread isCancelled] )
{
return;
}
Regards,
hris.to
In View and Window Architecture is stated, quote:
Views work in conjunction with Core Animation layers to handle the rendering and animating of a view’s content. Every view in UIKit is backed by a layer object (usually an instance of the CALayer class), which manages the backing store for the view and handles view-related animations.
Farther in the "The View Drawing Cycle" section is stated:
The UIView class uses an on-demand drawing model for presenting content. When a view first appears on the screen, the system asks it to draw its content. The system captures a snapshot of this content and uses that snapshot as the view’s visual representation.
Does that mean, that the content drawn in a view in its drawRect method call, is captured in a snapshot in saved in its backing core animation layer?
If not, where do this content snapshot "reside"?
If not, does that mean that CALayer is used to render "static" content, content that doesn't change very often, and drawRect is used to render content that changes often, for example in a game app?
p.s.
The questions are not related to any particular code implementation.
I just want to understand the ios view-layer architecture.
Does that mean, that the content drawn in a view in its drawRect method call, is captured in a snapshot in saved in its backing core animation layer?
Yes. Everything uses layers under the hood. UIView's -drawRect will capture what you draw and (as far as I know) set the content on a sublayer of the view. It might even do this on the main layer object. That's where the 'snapshot' is saved.
If not, does that mean that CALayer is used to render "static" content,
content that doesn't change very often, and drawRect is used to render
content that changes often, for example in a game app?
How often the content changes doesn't really affect the choice. There is not much difference in using drawRect vs. manually creating CALayers. It depends on how you want to organize the sub-elements in your views, or if you want to create low level reusable layer objects without the details of UIView (e.g. CATextLayer). If you have various different sub-elements to draw then you may split them into different layers with their own custom drawing code. If you just have one simple piece of content to draw, you can do that all in a single drawRect implementation.
Having said this, you do need to be aware that each layer will end up being a separate GPU "element", so there can be performance benefits to reducing the number of layers you have, or using the shouldRasterize+rasterizationScale properties of a parent layer. This will take a snapshot of an entire layer hierarchy and create a single rasterized image to be rendered instead of n separate ones.
Does that mean, that the content drawn in a view in its drawRect method call, is captured in a snapshot in saved in its backing core animation layer?
Two words: "implementation details"
If not, does that mean that CALayer is used to render "static" content, content that doesn't change very often, and drawRect is used to render content that changes often, for example in a game app?
Not exactly. Layers are very good at animating content (as hinted by the framework name Core Animation). drawRect is good for advanced drawing but can be to slow to redraw every frame (obviously depending on what your are drawing).
I didn't see any mention of the Core Animation Programming Guide in your question. It is a good place to learn more about the layer part of views.
Every UIView has an underlying CALayer (which is what actually gets rendered on the screen).
A CALayer is just a bitmap (holds pixels). When you call setNeedsDisplay to your view the CALayer gets marked for redrawing. At the end of the run loop, after events are processed, a CGContextRef gets created and the drawRect delegate gets called. You then draw things to the created context which then gets copied into the bitmap and ultimately composited with other layers to be displayed on the screen.
So yes, the "snapshot" is stored in the CALayer. This is just an optimization so that the layers don't have to redraw themselves unless they're flagged to be redrawn (with setNeedsDisplay).
In iOS, is there a relationship between run loop and display refresh? If so how?
Apple has a very good WWDC 2012 video describing in detail what happens at the end of each run loop when the current CATransaction commits and drawing happens and animations begin. You might also enjoy reading the less technical explanation of the "redraw moment" in my book:
http://www.apeth.com/iOSBook/ch17.html#_drawing_animation_and_threading
Apple's View Programming Guide for iOS / View and Window Architecture / View Architecture Fundamentals / The View Drawing Cycle says this:
When the contents of your view change, you do not redraw those changes directly. Instead, you invalidate the view using either the setNeedsDisplay or setNeedsDisplayInRect: method. These methods tell the system that the contents of the view changed and need to be redrawn at the next opportunity. The system waits until the end of the current run loop before initiating any drawing operations. This delay gives you a chance to invalidate multiple views, add or remove views from your hierarchy, hide views, resize views, and reposition views all at once. All of the changes you make are then reflected at the same time.
(Emphasis added.)
I have a view that has many subviews and sublayers that are built up from a background thread using NSOperation. Then I invoked back to the main thread when adding them to the UIView that is already being displayed. When everything is done rendering, performance and responsiveness is great. However, the main thread seems to be taking a very long time to do the initial render of my views, causing performance issues while the initial render is taking place.
I was able to get around this performance issue if my NSOperation added the subviews/sublayers to a currently visible UIView, but it would not appear until it was tapped on. Calling the setNeedsDisplay method did not resolve this issue.
I am wondering if their is a way to tell the main thread that I am adding a bunch of views so that it knows to do this more optimally? Somthing similar to an addSubviewRange instead of addSubview?