Using NSFont with AttributeContainer

465 views Asked by At

When creating an AttributedString for use in AppKit I get a warning that NSFont isn't Sendable:

    var container = AttributeContainer()
    container.appKit.foregroundColor = .red
    container.appKit.font = .systemFont(ofSize: mySize)
//  ^ Conformance of 'NSFont' to 'Sendable' is unavailable

The warning is correct that NSFont isn't Sendable, so is there a way to accomplish this without turning off concurrency warnings? AppKit is well behind SwiftUI and UIKit when it comes to being audited for Sendable conformance, but there's no much I can do about it. Marking the import of Foundation as @preconcurrency has no effect. A quick test project shows that the font is set properly and can be used in, say, an NSTextView. I just don't want to have to stare at those warnings until Apple gets around to AppKit refinement (historically, could be quite a while).

EDIT: I'm using Xcode 15b5. Xcode 14.3.1 doesn't show the warning.

4

There are 4 answers

1
vadian On BEST ANSWER

You can initialize the container with a [NSAttributedString.Key : Any] dictionary. This doesn't show the warning

let container = AttributeContainer([.foregroundColor: Color.red, 
                                    .font: NSFont.systemFont(ofSize: mySize)])
3
KFDoom On

Edited for misunderstanding:

Wrapping NSFont in another struct like MainThreadFont wouldn't eliminate the compiler warning since NSFont isn't declared Sendable. I made an oversight.

Unfortunately, as far as I know there isn't a perfect solution for this problem. For now, if you're certain about your NSFont usage being safe across different threads, you might need to suppress the warning using @unchecked Sendable attribute, but please use this sparingly and only when you're absolutely certain about thread safety, as this bypasses Swift's concurrency safety checks.

Here's an example:

struct UncheckedSendableFont: Sendable {
    @MainActor
    private let font: NSFont

    init(_ font: NSFont) {
        self.font = font
    }

    @MainActor
    var value: NSFont {
        return font
    }
}

Then use it like so:

var container = AttributeContainer()
container.appKit.foregroundColor = .red
container.appKit.font = UncheckedSendableFont(.systemFont(ofSize: mySize)).value

This will prevent the concurrency warning from appearing.

Please note that this effectively tells Swift to ignore the Sendable conformance issue for NSFont. It should only be used if you're completely confident that your usage of NSFont will not cause any concurrency issues. As a best practice, always ensure that NSFont instances are only accessed on the main thread, and be very careful about passing them between threads.

0
smr On

There doesn't seem to be a great way to deal with this. I filed a feedback (FB12885931) on the issue but...you know.

There is reason to be fairly confident in marking NSFont as Sendable as it's documented in the Cocoa Text Architecture Guide as follows: "Font objects are immutable, so it is safe to use them from multiple threads in your app." Hopefully Apple will audit AppKit for concurrency issues more thoroughly sometime soon.

1
mkeiser On

The following got rid of the warning for me in Xcode 15.0 beta 6:

    var container = AttributeContainer()
    container.appKit.foregroundColor = .red
    // Produces a value of type `AttributeScopes.SwiftUIAttributes.FontAttribute.Value`
    container.appKit.font = .init(.systemFont(ofSize: mySize))