iOS 12, Xcode 10: UIView setNeedsDisplay(_:) seems to be broken

After updating to Xcode 10.0 I realized that the draw(_ rect: CGRect) routine of my custom UIView (class derived from UIView) in my application was called with the wrong rect. Indeed it is always called with rect being the full frame of the underlying UIView, instead of the rect being specified by setNeedsDisplay(_ rect: CGRect).


Here is a code snippet that can be run as a playground, which at least in my setup shows the erroneous behavior described above in a minimalistic setting:


import Foundation
import UIKit
import PlaygroundSupport

class CustomView: UIView {
    override func draw(_ rect: CGRect) {
        print("rect = \(rect)")
    }
}

let customView = CustomView(frame: CGRect(origin: CGPoint.zero, size: CGSize(width: 200.0, height: 200.0)))
PlaygroundPage.current.liveView = customView
print("test")
customView.setNeedsDisplay(CGRect(origin: CGPoint.zero, size: CGSize(width: 100.0, height: 100.0)))


The output I get is

rect = (0.0, 0.0, 200.0, 200.0)
test
rect = (0.0, 0.0, 200.0, 200.0)


The first printed output for rect is the standard full redraw of the view, but the second one after printing "test" shows the problem. This second output is from redrawing due to the customView.setNeedsDisplay call and should be the smaller specified rectangle (0.0, 0.0, 100.0, 100.0).


So my obvious questions are:

  • Can you reproduce this behavior?
  • Am I missing something obvious?
  • Is this a bug?

Replies

XCode 10.2 beta 2, still not fixed!

This isn't a bug. (I thought this got mentioned in a different thread, but perhaps I misremember.)


iOS 12 and macOS 10.14 both had a significant behavioral change in the way drawRect:(Obj-C)/draw(Swift) are handled. Now, by default, drawing decides the depth of the backing store dynamically, based on what you draw. This means it needs to know what the view contains, which in turn means it needs you to draw everything in the view explicitly.


Therefore, you can now expect that you will always be asked to draw the entire view, instead of just a sliver. Behind the scenes, the new drawing mechanism is designed to optimize the process, and to clip away the unwanted portions later. It turns out that this is generally more efficient than drawing part of the view to a deeper backing store than it needs.


The only situation where you will lose by this change is when your drawRect does expensive calculations that can be avoided if you know you're not redrawing the relevant part(s) of the view. It has always been discouraged to do such things is drawRect (better to do the expensive calculations in advance). After all, there were always situations where the entire view had to be redrawn — you can't eliminated "slow" redraws completely.


If you're forced to keep using this pattern, you can disable the dynamic backing store assignment by setting the view's layer's "contentFormat" property to a suitable value. If you do that, you should see the expected "dirtyRect" values. However, I don't recommend you do that. Better stick with the default behavior, and look for other ways to mitigate any view drawing performance issues.


This change is mentioned in WWDC videos such as here:


https://developer.apple.com/videos/play/wwdc2018/219?time=1451


(that's around the 14:10 mark).

I meet same thing , are there any solution about this?

You could override setNeedsDisplay in your UIView subclass like this:


override func setNeedsDisplay(_ rect: CGRect) {
    self.draw(rect)
}


This is not really recommended for performance reasons, but serves as a quick fix. I will look further into QuinceyMorris answer.

Thanks for your answer.But I don't think this can fix the problem.Now I use a NSMutableArray in my subclass of UIView to quick fix that. I know this may not be quite good , but solve my problem at last.

- (void)setNeedsDisplayInRect:(CGRect)rect {
    [self.rectQueue addObject:[NSValue valueWithCGRect:rect]];
    [super setNeedsDisplayInRect:rect];
}

- (void)drawRect:(CGRect)dummyRect
{
    CGRect rect = dummyRect;
    if(self.rectQueue.count > 0) {
        rect = [[self.rectQueue objectAtIndex:0]CGRectValue];
        [self.rectQueue removeObjectAtIndex:0];
    }
    // use rect dosomething here
}

I need to redraw only certain area of a view frequently where maintaining dynamic backing store and redrawing to its entire rect wont be optimal so i had to disable dynamic backing store. Setting Layer's contentFormat to kCAContentsFormatRGBA16Float disables dynamic backing store and calls draw(_:) with specified rect, but it provides two awkward behaviours

  1. Dynamic backing store gets disabled only if we set layer's contentFormat inside draw(_:)
  2. draw(_:) maintains same origin but size(width,height) increased to .5 points with respect to specified rect


Trying this for an tvos app believing that wont make any difference.