macOS 应用程序在 Sonoma (macOS 14) 上的 UI 已损坏,但仅在使用 Xcode 15 构建时出现问题

问题描述 投票:0回答:1

当我使用 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 macos user-interface appkit macos-sonoma
1个回答
0
投票

首先,问题与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。默认值的更改认识到,剪裁视图的后代比取消剪裁视图的祖先要容易得多。

来源:AppKit 14 发布说明

这主要影响实现自己的

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 来调整此模式。

来源:AppKit 14 发布说明

到目前为止,这个填充指令只能填充整个视图或其子矩形。但如果不再进行裁剪,它可能会填充其超级视图的大部分甚至整个窗口!

苹果提出了两个如何解决这个问题的建议,但恕我直言,这些都是不好的建议。

  1. 重新启用

    clipToBounds
    将解决问题,因为随后
    dirtyRect
    会再次被剪辑,并且您会得到旧的行为。但是,请记住,您的自定义绘图视图也可以在 Interface Builder 中使用(在 XIB 文件中),并且在 Interface Builder 中,
    Clips Bounds
    是一个可设置的属性,因此有人使用您的视图,而不知道它依赖于该属性剪切行为(因为实际上并不希望出现这种情况),可能会错误地设置属性,然后不知道为什么整个 UI 又消失了。

  2. 始终填充整个边界当然可以正确工作,但这只是过度。

    dirtyRect
    也可能小于视图的边界,如果只有一小部分确实需要重绘,那么没有理由需要填充屏幕的大面积。

恕我直言,最好的解决方案是简单地自己剪辑边界:

- (void)drawRect:(NSRect)dirtyRect
{
    NSRect clippedRect = NSIntersectionRect(dirtyRect, self.bounds);
    [[NSColor ...] set];
    NSRectFill(clippedRect);
}

clippedRect
是描述您的边界和
dirtyRect
共同区域的矩形。在最坏的情况下,它会等于你的界限,但它永远不会比这个更大。在最好的情况下,这将是您边界的一个小分区,然后只有该分区需要填充。

© www.soinside.com 2019 - 2024. All rights reserved.