What is the most robust way to force a UIView to redraw?

Solution 1:

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.

Solution 2:

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.

Solution 3:

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.

Solution 4:

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.

Solution 5:

You can use CATransaction to force a redraw:

[CATransaction begin];
[someView.layer displayIfNeeded];
[CATransaction flush];
[CATransaction commit];