119

I have a UITableView with a list of items. Selecting an item pushes a viewController that then proceeds to do the following. from method viewDidLoad I fire off a URLRequest for data that is required by on of my subviews - a UIView subclass with drawRect overridden. When the data arrives from the cloud I start building my view hierarchy. the subclass in question gets passed the data and it's drawRect method now has everything it needs to render.

But.

Because I don't call drawRect explicitly - Cocoa-Touch handles that - I have no way of informing Cocoa-Touch that I really, really want this UIView subclass to render. When? Now would be good!

I've tried [myView setNeedsDisplay]. This kinda works sometimes. Very spotty.

I've be wrestling with this for hours and hours. Could someone who please provide me with a rock solid, guaranteed approach to forcing a UIView re-render.

Here is the snippet of code that feeds data to the view:

// Create the subview
self.chromosomeBlockView = [[[ChromosomeBlockView alloc] initWithFrame:frame] autorelease];

// Set some properties
self.chromosomeBlockView.sequenceString     = self.sequenceString;
self.chromosomeBlockView.nucleotideBases    = self.nucleotideLettersDictionary;

// Insert the view in the view hierarchy
[self.containerView          addSubview:self.chromosomeBlockView];
[self.containerView bringSubviewToFront:self.chromosomeBlockView];

// A vain attempt to convince Cocoa-Touch that this view is worthy of being displayed ;-)
[self.chromosomeBlockView setNeedsDisplay];

Cheers, Doug

Rob Napier
  • 250,948
  • 34
  • 393
  • 528
dugla
  • 12,300
  • 23
  • 79
  • 127

6 Answers6

195

The guaranteed, rock solid way to force a UIView to re-render is [myView setNeedsDisplay]. If you're having trouble with that, you're likely running into one of these issues:

  • You're calling it before you actually have the data, or your -drawRect: is over-caching something.

  • You're expecting the view to draw at the moment you call this method. There is intentionally no way to demand "draw right now this very second" using the Cocoa drawing system. That would disrupt the entire view compositing system, trash performance and likely create all kinds of artifacting. There are only ways to say "this needs to be drawn in the next draw cycle."

If what you need is "some logic, draw, some more logic," then you need to put the "some more logic" in a separate method and invoke it using -performSelector:withObject:afterDelay: with a delay of 0. That will put "some more logic" after the next draw cycle. See this question for an example of that kind of code, and a case where it might be needed (though it's usually best to look for other solutions if possible since it complicates the code).

If you don't think things are getting drawn, put a breakpoint in -drawRect: and see when you're getting called. If you're calling -setNeedsDisplay, but -drawRect: isn't getting called in the next event loop, then dig into your view hierarchy and make sure you're not trying to outsmart is somewhere. Over-cleverness is the #1 cause of bad drawing in my experience. When you think you know best how to trick the system into doing what you want, you usually get it doing exactly what you don't want.

Community
  • 1
  • 1
Rob Napier
  • 250,948
  • 34
  • 393
  • 528
  • Rob, Here's my checklist. 1) Do I have the data? Yes. I create the view in a method - hideSpinner - called on the main thread from the connectionDidFinishLoading: thusly: [self performSelectorOnMainThread:@selector(hideSpinner) withObject:nil waitUntilDone:NO]; 2) I don't need drawing instantly. I just need it. Today. Currently, it is completely random, and out of my control. [myView setNeedsDisplay] is completely unreliable. I've even gone so far as to call [myView setNeedsDisplay] in viewDidAppear:. Nuthin'. Cocoa simply ignores me. Maddening!! – dugla Oct 01 '09 at 13:33
  • Looking at your code, the first thing you want to make sure of is that contatinerView is itself on the screen. Put something else in it (a UILabel for instance) and see if that draws. Second, make sure that self.containerView is not nil. A primary cause of "nothing happens" is sending a message to nil. You should not need a setNeedsDisplay here, but if you did it would be more typical to send it to containerView rather than chromosomeBlockView. You're asking the containerView to redraw itself and its subviews (which you have rearranged), one of which happens to be chromosomeBlockView. – Rob Napier Oct 01 '09 at 17:37
  • 1
    I've found that with this approach there's often a delay between the `setNeedsDisplay` and the actual call of `drawRect:`.  While the call reliability is here, I wouldn't call this the “most robust” solution— a robust drawing solution should, in theory, do all the drawing immediately before returning back to the client code that's demanding the draw. – Slipp D. Thompson May 08 '13 at 02:36
  • 1
    I commented below on your answer with more details. Trying to force the drawing system to run out of order by calling methods the documentation explicitly says not to call is not robust. At best it is undefined behavior. Most likely it will degrade performance and drawing quality. At worst it could deadlock or crash. – Rob Napier May 08 '13 at 04:22
  • 1
    If you need to draw before returning, draw into an image context (which works just like the canvas many people expect). But don't try to trick the compositing system. It's designed to provide high quality graphics very fast with a minimum of resource overhead. Part of that is coalescing drawing steps at the end of the run loop. – Rob Napier May 08 '13 at 04:25
  • 1
    @RobNapier I don't understand why you're becoming so defensive. Please check again; there is no API violation (although it's been cleaned up based on your suggestion I'd argue calling one's own method isn't a violation), nor chance of deadlock or crash. It's also not a “trick”; it uses CALayer's setNeedsDisplay/displayIfNeeded in a normal fashion. Furthermore, I've been using it for quite a while now to draw with Quartz in a manner parallel to GLKView/OpenGL; it's proved safe, stable, and faster than the solution you've listed. If you don't believe me, try it. You have nothing to lose here. – Slipp D. Thompson May 08 '13 at 17:06
  • @RobNapier Update: I've been using my `CALayerDelegate`-based approach now since iOS 6.x all the way through to current (iOS 13.5) and never had a deadlock, crash, hiccup, or any other issue— and haven't had to change the code one bit in those 7 years. If that isn't stable, reliable, & robust, I don't know what is. P.S. I never got a reply from you when I explained that UIView doesn't have a `- display` method (you were thinking of CALayer; and UIView gained a `- displayLayer:` method in iOS 10), and no methods marked do-not-call were used… from a former community manager, I expected better. – Slipp D. Thompson May 24 '20 at 04:34
53

I had a problem with a big delay between calling setNeedsDisplay and drawRect: (5 seconds). It turned out I called setNeedsDisplay in a different thread than the main thread. After moving this call to the main thread the delay went away.

Hope this is of some help.

Werner Altewischer
  • 8,904
  • 4
  • 47
  • 54
  • This was definitely my bug. I was under the impression that I was running on the main thread, but it wasn't until I NSLogged NSThread.isMainThread that I realized there was a corner case where I was NOT making the layer changes on the main thread. Thanks for saving me from pulling out my hair! – Mr. T Jun 03 '13 at 05:34
16

The money-back guaranteed, reinforced-concrete-solid way to force a view to draw synchronously (before returning to the calling code) is to configure the CALayer's interactions with your UIView subclass.

In your UIView subclass, create a displayNow() method that tells the layer to “set course for display” then to “make it so”:

Swift

/// Redraws the view's contents immediately.
/// Serves the same purpose as the display method in GLKView.
public func displayNow()
{
    let layer = self.layer
    layer.setNeedsDisplay()
    layer.displayIfNeeded()
}

Objective-C

/// Redraws the view's contents immediately.
/// Serves the same purpose as the display method in GLKView.
- (void)displayNow
{
    CALayer *layer = self.layer;
    [layer setNeedsDisplay];
    [layer displayIfNeeded];
}

Also implement a draw(_: CALayer, in: CGContext) method that'll call your private/internal drawing method (which works since every UIView is a CALayerDelegate):

Swift

/// Called by our CALayer when it wants us to draw
///     (in compliance with the CALayerDelegate protocol).
override func draw(_ layer: CALayer, in context: CGContext)
{
    UIGraphicsPushContext(context)
    internalDraw(self.bounds)
    UIGraphicsPopContext()
}

Objective-C

/// Called by our CALayer when it wants us to draw
///     (in compliance with the CALayerDelegate protocol).
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)context
{
    UIGraphicsPushContext(context);
    [self internalDrawWithRect:self.bounds];
    UIGraphicsPopContext();
}

And create your custom internalDraw(_: CGRect) method, along with fail-safe draw(_: CGRect):

Swift

/// Internal drawing method; naming's up to you.
func internalDraw(_ rect: CGRect)
{
    // @FILLIN: Custom drawing code goes here.
    //  (Use `UIGraphicsGetCurrentContext()` where necessary.)
}

/// For compatibility, if something besides our display method asks for draw.
override func draw(_ rect: CGRect) {
    internalDraw(rect)
}

Objective-C

/// Internal drawing method; naming's up to you.
- (void)internalDrawWithRect:(CGRect)rect
{
    // @FILLIN: Custom drawing code goes here.
    //  (Use `UIGraphicsGetCurrentContext()` where necessary.)
}

/// For compatibility, if something besides our display method asks for draw.
- (void)drawRect:(CGRect)rect {
    [self internalDrawWithRect:rect];
}

And now just call myView.displayNow() whenever you really-really need it to draw (such as from a CADisplayLink callback).  Our displayNow() method will tell the CALayer to displayIfNeeded(), which will synchronously call back into our draw(_:,in:) and do the drawing in internalDraw(_:), updating the visual with what's drawn into the context before moving on.


This approach is similar to @RobNapier's above, but has the advantage of calling displayIfNeeded() in addition to setNeedsDisplay(), which makes it synchronous.

This is possible because CALayers expose more drawing functionality than UIViews do— layers are lower-level than views and designed explicitly for the purpose of highly-configurable drawing within the layout, and (like many things in Cocoa) are designed to be used flexibly (as a parent class, or as a delegator, or as a bridge to other drawing systems, or just on their own). Proper usage of the CALayerDelegate protocol makes all this possible.

More information about the configurability of CALayers can be found in the Setting Up Layer Objects section of the Core Animation Programming Guide.

Slipp D. Thompson
  • 28,297
  • 3
  • 40
  • 41
  • Do note that the documentation for `drawRect:` explicitly says "You should never call this method directly yourself." Also, CALayer's `display` explicitly says "Do not call this method directly." If you want to synchronously draw on the layers `contents` directly there is no need to violate these rules. You can just draw onto the layer's `contents` anytime you want (even on background threads). Just add a sublayer to the view for that. But that is different than putting it on the screen, which needs to wait until the correct compositing time. – Rob Napier May 08 '13 at 04:05
  • I say to add a sublayer since the docs also warn about messing directly with a UIView's layer's `contents` ("If the layer object is tied to a view object, you should avoid setting the contents of this property directly. The interplay between views and layers usually results in the view replacing the contents of this property during a subsequent update.") I'm not particularly recommending this approach; premature drawing undermines performance and drawing quality. But if you need it for some reason, then `contents` is how to get it. – Rob Napier May 08 '13 at 04:13
  • @RobNapier Point taken with calling `drawRect:` directly.  It wasn't necessary to demonstrate this technique, and has been fixed. – Slipp D. Thompson May 08 '13 at 16:48
  • @RobNapier As for the `contents` technique you're suggesting… sounds enticing.  I tried something like that initially but couldn't get it to work, and found the above solution to be much less code with no reason not to be just as performant.  However, if you do have a working solution for the `contents` approach, I'd be interested in reading it (there's no reason why you can't have two answers to this question, right?) – Slipp D. Thompson May 08 '13 at 16:52
  • @RobNapier Note: This doesn't have anything to do with CALayer's `display`. It's just a custom public method in a UIView subclass, just like what is done in GLKView (another UIView subclass, and the only Apple-written one I know of that demands _DRAW NOW!_ functionality). – Slipp D. Thompson May 08 '13 at 16:55
5

I had the same problem, and all the solutions from SO or Google didn't work for me. Usually, setNeedsDisplay does work, but when it doesn't...
I've tried calling setNeedsDisplay of the view just every possible way from every possible threads and stuff - still no success. We know, as Rob said, that

"this needs to be drawn in the next draw cycle."

But for some reason it wouldn't draw this time. And the only solution I've found is calling it manually after some time, to let anything that blocks the draw pass away, like this:

dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, 
                                        (int64_t)(0.005 * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void) {
    [viewToRefresh setNeedsDisplay];
});

It's a good solution if you don't need the view to redraw really often. Otherwise, if you're doing some moving (action) stuff, there is usually no problems with just calling setNeedsDisplay.

I hope it will help someone who is lost there, like I was.

dreamzor
  • 5,645
  • 4
  • 36
  • 58
0

Well I know this might be a big change or even not suitable for your project, but did you consider not performing the push until you already have the data? That way you only need to draw the view once and the user experience will also be better - the push will move in already loaded.

The way you do this is in the UITableView didSelectRowAtIndexPath you asynchronously ask for the data. Once you receive the response, you manually perform the segue and pass the data to your viewController in prepareForSegue. Meanwhile you may want to show some activity indicator, for simple loading indicator check https://github.com/jdg/MBProgressHUD

Miki
  • 1
0

I'm using CATransaction to force a redraw:

[CATransaction begin];
[someView.layer displayIfNeeded];
[CATransaction flush];
[CATransaction commit];
Slyv
  • 341
  • 2
  • 8