Wednesday, June 28, 2023

View Clipping Changes in macOS 14

WWDC 2023 session 10054:

NSViews clip their drawing contents to their bounds. That sometimes leads to drawing not displaying the way you want, like the bottom of this Hindi glyph in a FreeForm alert window.

Common places this can occur is with font rendering, shadows, or other sub-view accents, like a badge or a flame on that “hot” item for sale. There are ways to solve this. For example, embed the combined views as siblings in a larger view. However, each technique has its own drawbacks. In this case, combining the enclosing view with a button in a simple horizontal stack doesn’t line up the base lines of the text by default. And now you have another problem to solve.

There is a better way. When linked on macOS Sonoma, most NSViews no longer clip to their bounds by default. Hit testing remains unchanged and is determined by the geometry of the view. Of course, you can override hitTest to change this. Now that a view may draw outside of its bounds, its calculated visibleRect may also extend past its bounds. Review any code that uses visibleRect and adjust accordingly. This also impacts the dirtyRect parameter of the draw function. Specifically, the dirtyRect is not constrained by a view’s bounds.

AppKit reserves the right to pass a dirtyRect that is larger than the view’s bounds. AppKit also reserves the right to subdivide drawing into as many rectangles as it needs. What this means for you is that you should use the dirtyRect to decide what to draw, not where to draw.

See also: the release notes.

Daniel Jalkut (Mastodon):

Apple announced that a long-present internal property, “clipsToBounds”, is now public. In tandem with this change, they are changing the default value for this property to false for apps that link against the macOS 14 SDK.

[…]

What’s happening here is my custom calendar view is filling the “dirtyRect” with its background color before continuing to draw the rest of its content. Unfortunately, what Apple promised in the excerpt above has come to pass: the dirtyRect encompasses nearly the entire window! So when my calendar view redraws, it is evidently overwriting the “OK” button, as well as a date picker that is supposed to appear below the calendar.

[…]

The fix is relatively simple in this case: because I don’t anticipate the view being very large, and filling a rect is a pretty cheap operation, I simply changed it to fill “self.bounds” instead of “dirtyRect”.

Daniel Jalkut:

Even though this took my surprise, and even though I do think it’ll be the most pervasive source of 3rd-party bugs in Sonoma, I do think it’s a good change. The collective time wasted working around subtle clipping issues is far greater than the time spent working around this one-time change.

Erik Schwiebert:

I think best-practice if you have a custom control that doesn’t draw outside its bounds is to set the clipToBounds property to YES, and let the OS decide the optimal rect to hand you like it always did. If you do have a control that draws outside its bounds, then the new behavior makes things much easier—instead of playing games with nested views etc to get more drawing space you can just do it directly in the actual control!

Update (2023-10-30): See also: Core Intuition.

Update (2023-12-19): Andy Lee:

“Recognizes”? “Vastly”? Do most people agree with this? Can someone please point to examples where this change made their life easier? I’m genuinely confused.

5 Comments RSS · Twitter · Mastodon

"When linked on macOS Sonoma, most NSViews no longer clip to their bounds by default."

I'm not a Mac developer, but this seems fairly developer-hostile to me. Why not let developers opt-in to the new behavior, so apps keep working the same way without updates? Daniel Jalkut says that this change will be "the most pervasive source of bugs in existing apps". How is it a bug in an app to expect that a non-deprecated component would maintain its behavior? Sounds like an OS bug to me.

Old Unix Geek

@Nick: Dev & user hostile. To quote Linus: "do not break userspace". Same should apply here.

I guess the thinking is that the new behavior is a better in most cases, and a lot of people could benefit from it but might not know about this obscure setting that could solve their problem. Or they might not know they have a problem because it only manifests with certain localizations or user data. So changing the default is a practical way to make most stuff work better with less ongoing effort. The SDK version check is a sort of a half opt-in because compiled code does keep working, but eventually you will need to build with the new SDK and then you have to opt out if you don’t want the new behavior. Apple does this sort of thing with lots of APIs, and I think it mostly works pretty well.

"so apps keep working the same way without updates"

That's exactly what will happen with apps that haven't been built specifically for Sonoma. It's only when the app is built for Sonoma that the new behavior kicks in. When you install Sonoma, your old third party apps will continue behaving as they always had.

Been a while since I wrote AppKit code: why exactly clipping outside NSView was problematic, weren't you always supposed to size views properly to allow for accents and such? How is it going to interoperate with stuff like NSView.wantsDefaultClipping?

Leave a Comment