当我使用 Xcode 15 构建 UI 应用程序并在 macOS 14 (Sonoma) 上运行该应用程序时,多个视图中的 UI 大部分缺失。在 macOS 14 之前的任何系统上运行相同的构建时,UI 看起来都很好。使用 Xcode 14.3.1 构建并在 macOS 14 上运行应用程序时,UI 看起来也很好。只是 Xcode 15 和 macOS 14 的组合导致了这种情况问题。
作为一种解决方法,我继续使用 Xcode 14.3.1 构建应用程序,即使我使用的是 Sonoma 并且 Xcode 14.3.1 甚至不支持 Sonoma,但使用 这个技巧 我还是能够让它工作。这暂时解决了问题,但并不是真正的长期解决方案,因为在未来的某个时候,升级到更新的 Xcode 版本将不可避免。
首先,问题与Xcode版本无关,而是与SDK版本有关。 Xcode 15 附带 macOS 14 SDK,而 Xcode 14.3.1 仅附带 macOS 13 SDK。
问题的原因是 AppKit 中的更改,该更改只会影响与 macOS 14 SDK 链接的应用程序,并且仅当也在 macOS 14 上运行时,这解释了为什么在较旧的 macOS 版本上运行或使用较旧的 SDK。
macOS 14 AppKit 发行说明中提到了这一变化:
在 macOS 14 中,AppKit 公开了 NSView 的 ClipsToBounds 属性。
对于与 macOS 14 SDK 链接的应用程序,此属性的默认值为 false。与旧版 SDK 链接的应用程序默认为 true。某些类(例如 NSClipView)继续默认为 true。默认值的更改认识到,剪裁视图的后代比取消剪裁视图的祖先要容易得多。
这主要影响实现自己的
NSView
方法的 drawRect:(NSRect)dirtyRect
子类。 dirtyRect
描述了需要重绘的屏幕数组,并将其传递给该方法,因此任何绘图代码都可以将其绘图操作限制为仅该区域。
这只是速度优化,因为总是重绘整个视图也会产生正确的结果,但它可能会导致视图绘制超出实际需要的程度。这就是为什么许多简单的视图甚至不考虑
dirtRect
并且总是重绘所有内容,但这样他们就白白浪费了 CPU/GPU 资源。
到目前为止
clipsToBounds
一直都是正确的。这意味着在调用此方法之前,系统会将 dirtyRect
剪切到视图的边界。因此,任何传递给视图的 dirtRect
要么是视图当前边界的子矩形,要么在最坏的情况下等于边界矩形,在这种情况下,整个视图必须重新绘制。但默认情况下不再是这种情况,这意味着 dirtyRect
的一部分可以位于视图的边界框之外,或者 dirtyRect
也可以比整个视图大得多,从而完全包围它。
当系统想要更新整个窗口时,它可以只使用包围整个窗口或更大块的
dirtRect
,并且不裁剪到边界,以下代码将导致UI问题:
- (void)drawRect:(NSRect)dirtyRect
{
[[NSColor ...] set];
NSRectFill(dirtyRect);
}
Apple 在发行说明中也解决了这个问题:
填充 -drawRect 内视图的脏矩形。一个相当常见的模式是简单地用矩形填充传递给 NSView.draw() 重写的脏矩形。脏矩形现在可以延伸到视图的边界之外。可以通过填充边界而不是脏矩形或通过设置 ClipsToBounds = true 来调整此模式。
到目前为止,这个填充指令只能填充整个视图或其子矩形。但如果不再进行裁剪,它可能会填充其超级视图的大部分甚至整个窗口!
苹果提出了两个如何解决这个问题的建议,但恕我直言,这些都是不好的建议。
重新启用
clipToBounds
将解决问题,因为随后 dirtyRect
会再次被剪辑,并且您会得到旧的行为。但是,请记住,您的自定义绘图视图也可以在 Interface Builder 中使用(在 XIB 文件中),并且在 Interface Builder 中,Clips Bounds
是一个可设置的属性,因此有人使用您的视图,而不知道它依赖于该属性剪切行为(因为实际上并不希望出现这种情况),可能会错误地设置属性,然后不知道为什么整个 UI 又消失了。
始终填充整个边界当然可以正确工作,但这只是过度。
dirtyRect
也可能小于视图的边界,如果只有一小部分确实需要重绘,那么没有理由需要填充屏幕的大面积。
恕我直言,最好的解决方案是简单地自己剪辑边界:
- (void)drawRect:(NSRect)dirtyRect
{
NSRect clippedRect = NSIntersectionRect(dirtyRect, self.bounds);
[[NSColor ...] set];
NSRectFill(clippedRect);
}
clippedRect
是描述您的边界和 dirtyRect
共同区域的矩形。在最坏的情况下,它会等于你的界限,但它永远不会比这个更大。在最好的情况下,这将是您边界的一个小分区,然后只有该分区需要填充。