drawRect broken in Xcode 15/Sonoma

I'm manually placing a subclass of NSView into the parent view using addSubview:positioned:relativeTo. The dirtyRect passed to drawRect: is wildly incorrect. Can folks attempt to reproduce and file bugs? This is awfully close to the release of Sonoma, and I feel like folks with bezier curves (or maybe other drawing code?) in their NSView subclasses are going to experience problems.

To reproduce, place a view (I'm using an NSImageView) as a subview within a view. Then, create a subclass of NSView and draw a bezier curve in the drawRect method. Add an instance of this subclass as a subview of your original view. I'm offsetting the x value for clarity. When I build with Xcode 15 and run on Ventura or earlier, I get the correct result. Or, if I build with Xcode 14.3 and run on Sonoma I get the correct result. However, when I build in Xcode 15 and run on the RC build of Sonoma, I get a whacky result.

I get something like (origin = (x = -264, y = -146), size = (width = 480, height = 388)) for the dirtyRect in the error case, while the rect is supposed to be (origin = (x = 0, y = 0), size = (width = 48, height = 48)) (I'm basing the frame of the new view on the original image.)

Thanks!

Answered by Frameworks Engineer in 765342022

Hi ski4funSonoma,

The dirtyRect behavior you're seeing is expected for a view with -[NSView clipsToBounds] set to NO, which is now the default for Sonoma-linked applications. The AppKit release notes touch on this, but here's a little more information.

The geometry of dirtyRect isn't too useful for determining where to draw your content. It's permissible for the dirtyRect to refer to any geometry that the view could draw but does not currently have fresh contents for. For unclipped views, that region is allowed to extend beyond the geometry of the view itself. Although this has always been part of -drawRect:'s API contract, it used to be very common to only see dirtyRect values that were equal to the bounds, so we've seen a number of cases where views accidentally rely on that specific behavior.

You should use the view's bounds as the basis for your path geometry. The dirtyRect is primarily useful for avoiding unnecessary work; for example, you could skip the cost of building a complicated Bézier path if you know it won't intersect the dirtyRect at all.

Probably it's not a bug, just the new behaviour. When linking to the macOS 14 SDK, the clipsToBounds property of NSView is set to false. Either update your drawing code, or set it back to true. See the macOS 14 AppKit release notes for some info.

Accepted Answer

Hi ski4funSonoma,

The dirtyRect behavior you're seeing is expected for a view with -[NSView clipsToBounds] set to NO, which is now the default for Sonoma-linked applications. The AppKit release notes touch on this, but here's a little more information.

The geometry of dirtyRect isn't too useful for determining where to draw your content. It's permissible for the dirtyRect to refer to any geometry that the view could draw but does not currently have fresh contents for. For unclipped views, that region is allowed to extend beyond the geometry of the view itself. Although this has always been part of -drawRect:'s API contract, it used to be very common to only see dirtyRect values that were equal to the bounds, so we've seen a number of cases where views accidentally rely on that specific behavior.

You should use the view's bounds as the basis for your path geometry. The dirtyRect is primarily useful for avoiding unnecessary work; for example, you could skip the cost of building a complicated Bézier path if you know it won't intersect the dirtyRect at all.

Thanks for the detailed explanation! It was helpful. 😊

I have lately bumped into the new Sonoma clipsToBounds behaviour. I must be missing something obvious, but it seems to me the current documentation now is at best misleading (if not plain wrong), and also it seems to me — far as the documentation is to be trusted — there's not any easy way to implement drawRect: anymore.

I'd be grateful is Quinn or anybody, who understands this properly, explains. Here are my problems:

  1. The drawRect: documentation, at this moment, says pretty explicitly

[dirty] rect ... The portion of the view’s bounds that needs to be updated. The first time your view is drawn, this rectangle is typically the entire visible bounds of your view. However, during subsequent drawing operations, the rectangle may specify only part of your view

This seems to me to be at odds with the Sonoma reality that the dirty rect now is allowed to extend beyond the geometry of the view itself. Do I understand it wrong?

This feels to me a documentation bug, looks like pre-Sonoma documentation which was not updated accordingly to the new behaviour, but as always, I can be wrong.

  1. Worse is, how do we implement drawRect: now (presumed we want to stick with the new clipsToBounds behaviour)?

Let's say we want simply a coloured background, the thing which pre-Sonoma was rightly and properly implemented by [colour set]; NSRectFill(dirtyRect); The release notes suggest we simply replace it with [colour set]; NSRectFill(self.bounds);This pattern can be adjusted by filling the bounds instead of the dirty rect, but — do please correct me if I am missing something of importance — this seems to be at odds with the drawRect: documentation, which very explicitly states (the above, and)

You should limit any drawing to the rectangle specified in the rect parameter

Given we draw whole self.bounds as suggested, and given the dirty rect very definitely can specify only part of our view, it seems plain wrong.

Actually, the sentence quoted above feels very strange to me. Far back as I can recall, it always used to be pretty usual to draw outside of the dirty rect (but inside of bounds).

I might have missed something of importance somewhere, but I've always though the dirty rect is just an optimising tool, which allow us to simplify complex drawing skipping anything which does not intersect it; but we should draw all parts which do intersect the dirty rect even if they exceed it (which many of them usually do). Besides, in simple cases, we can safely ignore the dirty rect completely and just fill the bounds as need be.

Is that not so anymore in Sonoma? I cannot recall the “You should limit any drawing...” sentence quoted above in the older documentation, but I might recall it wrong. Anyway, if that sentence is really meant to be taken into account, it seems to me we should always explicitly clip all our drawing to both bounds and dirty rect, which sometimes might be a bit non-trivial?

Thanks for any elucidation, OC

drawRect broken in Xcode 15/Sonoma
 
 
Q