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
Related
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.
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.
In my ViewController, I attempt to hide two images (currently displayed) at the same time, but after a delay of 3 seconds. I usr
[self performSelector:#selector(hideThem:) withObject:val afterDelay:3.0];
where "hideThem" is a routine that uses the following to hide the images. "val" is simply a NSNumber, not important to this question.
[image1 setHidden:YES];
[image2 setHidden:YES];
If I call "hideThem" directly (not using performSelector), both images disappear at the exact same time, which is the desired affect.
If, I use the performSelector, as shown, one image will hide, then after 0.5 seconds (or so) and the other image will hide. I do not have my own run loop. The images are UIImageView objects and are part of the view hierarchy under "self".
I assume this is an effect with how IOS handles timing of events, but I don't understand why the effect of the setHidden will occur with that 0.5 second delay when both should be set up as hidden "after" the performSelector call to "hideThem" fires.
What about IOS causes this behavior?
What are the recommended approaches to resolve this issue (so that, after 3 seconds, both images are hidden, visually, at the same time.
I can't figure out why such an issue would occur but I can give you an alternative using GCD:
double delayInSeconds = 3.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[self hideThem];
});
1)
I can see a colon : in method call #selector(hideThem:), double check whether your hideThem method takes input parameters.
2)
Make sure you call this [self performSelector:#selector(hideThem:) withObject:val afterDelay:3.0];
on main thread
try,
performSelectorOnMainThread
Ideas [I thought of 2 first but I suspect 1 will do the trick]:
1) Instead of setting their hidden property, use the UIView animation to turn both of their alphas to 0 over say a 100ms period, and in the completion block, set their hidden property to YES and reset their alphas back to 1.
2) Instead of hiding the two views immediately, create a new view to overlay them. You could even take a "snapshot" of what is under them and create an image out of that first:
create an image that you can use temporarily to overlay the two images you want to hide.
at the appropriate time, add that view to the hierarchy as the topmost view.
hide the two views under it
remove the temporary view
I found the culprit. Thanks for all the suggestions which helped me to isolate the issue. And, David H... very interesting ideas and I may actually have use for them as the project continues.
The answer is: after lots of searching, commenting out code, and tracking through things with logs I finally found ANOTHER related performSelector that was getting called elsewhere, with a timeframe of 2 seconds (hence, what I thought was showing a .5 sec delta between image hidings ).
In short... a strange coding bug.
Thanks again!
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 am using a NSFetchedResultsController to populate a UITableView. The fetch take some time so I would like to present a spinning wheel to the user while the fetch is under way.
How would I do that?
You should start your spinner in the Main thread and push the "heavy work" for a secondary thread. When the work is done, stop the spinner. You can achieve that with something like this:
// Start the spinning here.
[mySpinner startAnimating];
// Declare the queue
dispatch_queue_t workingQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// it's not, so we will start a background process to calculate it and not block the UI
dispatch_async(workingQueue,
^{
// Some heavy work here.
dispatch_async(dispatch_get_main_queue(), ^{
// stop the spinner here
[mySpinner stopAnimating];
});
});
Doing the following in the Main thread wont make you accomplish what you want:
Start Spinner => Heavy work => Stop Spinner
When the Heavy work begins, it will block your UI thread, so you won't actually see the UIActivityMonitor animating.
To finish I would advise you using this as spinner.
You have several way to do that :
had a UIBarItem if you use UINavigationBar and set customView to UIActivityIndicator at the beginning of your function and hide it a the end.
Create a "modal" view with a UIActivityIndicator at the center of the view, and add this subview to your table view and remove it a the end of your function.
formally just you can use the UIActivityIndicatorView to present the wheel and for some good and delicate way use the MBHoods from here download the demo app here