How can I reflect UIView changes of a setNeedsDisplay?
In drawRect triggered by setNeedsDisplay
- (void)drawRect:(CGRect)rect {
double i = 0;
for(...)
//i is incremented
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(i * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
drawStuff(element, context);
});
For example, each draw update would happen after 1s, 2s, 3s, ...
DrawStuff switches back to the main thread with
dispatch_async(dispatch_get_main_queue(), ^(void) { ...
when updating the UI. I have tried both UIBezierPath and Core Graphics and get the same results. I am also using UIGraphicsPushContext and UIGraphicsPopContext to get the correct context.
Using NSLog, I can tell the drawStuff is called in the 1s increments. However, the visible UIView remains unchanged. I can rotate the device screen and this will reposition the UIView and change its size. On rotation the UIView will reflect what it has currently drawn, but then I need to rotate it again to see the updated state after a few seconds.
Back to the question above, is there a call to tell UIView to display its current graphics after setNeedsDisplay? Thanks.
If you are trying to make sure your view is redrawn once per second, the right way to do this is to set some form of timer elsewhere in your view, and have the timer function call -setNeedsDisplay. This will in turn invalidate your view and cause your -drawRect: to be called, which should do the drawing (ie call your drawStuff)
Conceptually, -drawRect: should only be a dumb method that just knows how to do the actual drawing work based on whatever the current state of the view is. Your normal event logic (or a timer) outside of that should (a) update the state as necessary and (b) mark the view as needing to be redrawn.
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'm using this to move my image:
meteorDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(moveImage:)];
Here is my moveImage: method:
-(void) moveImage:(CADisplayLink *)sender{
CGPoint img;
imgLocation = imgImageView.center;
if (img < 100) {
img.y++;
}
else{
imgLocation.y = 0;
}
imgImageView.center = imgLocation;
}
This updates the location of my image on screen no matter what. Why does setNeedsDisplay have no effect here? What is the point of setNeedsDisplay?
I assume imgImageView is an instance of UIImageView. If it's not an instance of UIImageView, edit your question to tell us what it is.
Changing the center property of a view never requires your app to redraw the view. The display server (which is a separate process named backboardd in iOS 6 and springboard in earlier versions) has a copy of your view's pixel data already. When you change a view's center, the view just informs the display server of its new position, and the display server copies the view's pixel data to the appropriate parts of the screen.
Thus changing the center property of a view never requires you to send setNeedsDisplay.
More specifically, UIImageView doesn't even implement drawRect:. Instead, UIImageView just sends its image's pixel data to the display server when you set its image. So even if you send setNeedsDisplay to a UIImageView, it still won't run any drawRect: method.
By the way, you can do this in your own UIView subclasses too, by setting self.layer.contents to a CGImageRef instead of implementing drawRect:. You will need to #import <QuartzCore/QuartzCore.h> to do this. Of course, UIImageView does other things too, like handling animated and resizable images.
When you set properties that affect the layout of an object, setNeedsDisplay will be called internally. You need to call it yourself for custom properties, etc, that don't automatically call it.
setNeedsDisplay only applies to the contents of a layer or view. I.e., if the contents of a view has changed and needs to be redrawn, you would use setNeedsDisplay to tell the view that it needs to be redrawn and that would trigger its drawRect method.
Transformations on a view, such as position, scale, and rotation, etc., are handled directly by the GPU and do not require the view to be redrawn; therefore, setNeedsDisplay does not apply when you're simply moving or transforming a view.
I am trying to set a custom UIView class's background color. The class also does quartz drawing in the drawRect:method.
Since background color change does not take place until the next redraw of the view, I change the UIView's backgroundColor property before calling setNeedsDisplay. I have set a UIActivityIndicatorView to animate while the view is redrawing.
self.backgroundColor = theColor;
[indicator startAnimating];
[self performSelectorInBackground:#selector(setNeedsDisplay) withObject:nil];
The indicator is stopped at the end of setNeedsDisplay. theColor will change every time I need to call this.
Let's say I have a time consuming setNeedsDisplay process. I would like to set the background and keep the indicator animation. Currently, changing backgroundColor calls setNeedsDisplay but doesn't even change the backgroundColor until the performSelectorInBackground method runs! Therefore my app hangs and no indicator is ever animated.
How do I deal with this ordering problem? Thanks.
Edit: I meant that my drawrect: may be time consuming.
Let's say I have a time consuming setNeedsDisplay process
Let's not. You have no business overriding setNeedsDisplay. I am not at all clear on what you're ultimately trying to accomplish but this entire question seems to be a misunderstanding of how to draw. When you call setNeedsDisplay (which, as you've been told, you must do in the main thread), that's that; you stand out of the way, and when the redraw moment comes, your view's drawRect: is called. That's drawing.
If the problem is simply that the activity indicator never gets going, that's because you never give it a chance. It too is not going to start going until the redraw moment. But you are stopping the activity indicator before the redraw moment even comes! So obviously you'll never see it go.
The way to start an activity indicator visibly before the next thing you do is to step out to the main thread after the next redraw moment. This is called "delayed performance". Example:
self.backgroundColor = theColor;
[indicator startAnimating];
double delayInSeconds = 0.1;
dispatch_time_t popTime =
dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
// do something further, e.g. call setNeedsDisplay
};
You could extend that example by calling dispatch_after yet again to stop the indicator after the next redraw moment.
I must impress upon you, however, that if the mere act of drawing takes so long that you need an activity indicator to cover it, you're drawing wrong. Your act of drawing must be very very fast. You might want to watch the WWDC 2012 video on this very topic; it gives excellent tips on how to draw efficiently.
You can update UI only on Main thread, not in backgroung
Try to use another subview with activity indicator, put in on before redraw and remove from superview after
I have an app that fetches calendar events and displays data to the user. I'm getting some weird behavior when trying to update my labels.
I can fetch the calendar data just fine but when that gets done, my problem is that according to NSLog my label.text property has already changed, but it's another 4-8 seconds before the view gets redrawn.
Therefore, I'm trying to detect when the label gets redrawn, not when it's .text property changes so I can hide a progress view at the same time the data is populated in the labels.
I have already tried setNeedsDisplay and setNeedsLayout on self.view and the labels themselves. after the .text property of the labels has changed - doesn't work.
So unless I'm completely missing something about using setNeedsDisplay (which I understand only updates on the next redraw anyway), my question is, how do I detect when the UILabel and/or the UIView redraws itself?
How my app is setup:
I've been stuck on this for about 3 weeks.
Make sure setNeedsDisplay is being called on the main thread, using performSelectorOnMainThread:withObject:waitUntilDone:, for example:
[view performSelectorOnMainThread:#selector(setNeedsDisplay)
withObject:nil
waitUntilDone:NO];
Quote apple develop document :
The view is not actually redrawn until the next drawing cycle, at which point all invalidated views are updated.
maybe your main thread are blocking by other things , such as deal with many complex calculations
eg:
- (void)testMethod
{
myLabel.mytext = #"aaaa";
[myLabel setNeedsDisplay];
// some complex calculations
// the quickest , it will run finish the method then redraw.
}
I have a routine that draws a gearwheel using CoreGraphics and drawRect. I used a button to advanced the gears through the stages:
-(IBAction)advanceButtonPressed:(id)sender{
stage=stage+1;
if (stage==4) stage=0;
[self setNeedsDisplay];
}
The routine for drawing the gear is in drawRect, and the gear teeth are drawn in a new position depending on the vale of stage, which is passed to drawRect. In four steps, one per button press, the gear turns to its new position (one segment around the circle.)
I changed the buttonPressed routine to have it do the four stages in a loop, so I only need to press the button once, but it doesn't work:
-(IBAction)buttonPressed:(id)sender{
for (stage=0;stage<4;stage++){
NSLog(#"%i",stage);
[self setNeedsDisplay];
}
}
My NSLog shows that stage goes through its four values, just as it would in the earlier routine. But setNeedsDisplay is only executed once, (as a NSLog trace in drawRect confirms) so the gear is displayed in its final position but not the intermediate stages. It looks like setNeedsDisplay only executes when the button routine finishes, despite it being inside the loop.
Is there any way to force it to execute?
The iPhone only updates the screen 60 times a second no matter how often you call setNeedsDisplay. Your loop is going to execute in less than 1/60th of a second. That's why you only will see the last state.
If you want to show all four states through one button press then you could use a timer or performSelector: afterDelay to show all four states. A little more complicated but there you go.
Invoking setNeedsDisplay just marks the view to be redrawn at the next drawing cycle.
To redraw it during your method, you could explicitly call drawRect: