16

When I build my macOS app in Dark Mode, some of my text views (NSTextView) render black text on a almost-black background. In Interface Builder, in the Attributes Inspector, the "Text Color" is set to to the system "Default (Text Color)", which I believe is correct. Indeed, in Interface Builder, this text renders white in Dark Mode and black in Light Mode, as desired. I've searched my code for any lines where I might be progammatically setting the text color in this view to black, but can't find any. Why is my text always black?

Jerry Krinock
  • 4,860
  • 33
  • 39

4 Answers4

15

I noticed that the errant text views have their "attributed string" bound, with Cocoa Bindings, to methods which return plain, not attributed NSString objects. I probably did this because I was lazy when I wrote this app years ago, and it worked fine. This mismatch turned out to be the problem. The fix is to modify these methods to return a NSAttributedString, with an attribute dictionary containing the key/value pair

NSForegroundColorAttributeName : NSColor.controlTextColor

Probably what happened is that Cocoa was designed to do what you probably want when a attributed string binding gets a non-attributed string. Instead of barfing an exception, Cocoa applies some "default" attributes, which include the black text color which has been the macOS default since 1984 – totally sensible until Dark Mode came along! Well, it might have been nice of Apple to change this default from black to controlTextColor, but apparently they did not.

Conclusion: We can no longer get away with binding the attributed string of a text view to a plain non-attributed string.

Or, you can use the answer of @Ely and bind to value. But if you try that, and do not see a value binding in the Bindings Inspector, but do see a data binding, it is because of these remarks in the NSTextField documentation:

[value] binding is only available when the NSTextView is configured to display using as a single font.

and later

[data] binding is only available when the NSTextView is configured to allow multiple fonts.

It turns what they mean by configured to allow multiple fonts is that, in the Attributes inspector, the Allows Rich Text checkbox is on. Conversely, configured to display using as a single font means that the Allows Rich Text checkbox is off.

Jerry Krinock
  • 4,860
  • 33
  • 39
14

It worked for me after using this code (macOS Catalina version 10.15.3):

if #available(OSX 10.14, *) {         
    textView.usesAdaptiveColorMappingForDarkAppearance = true
} else {
    // Fallback on earlier versions - do nothing
}

I found this documented in the method:

/*************************** Dark Mode ***************************

When YES, enables the adaptive color mapping mode. In this mode under the dark effective appearance, NSTextView maps all colors with NSColorTypeComponentBased by inverting the brightness whenever they are coming in and out of the model object, NSTextStorage. For example, when rendering, interacting with NSColorPanel and NSFontManager, and converting from/to the pasteboard and external formats, the color values are converted between the model and rendering contexts. Note that the color conversion algorithm compresses the brightness range and, therefore, does not retain the round-trip fidelity between the light and dark appearances. It may not be suitable for rich text authoring, so it is a good idea to provide a command or preference for your users to see and edit their docs without this option, or in light mode. */

Douglas Frari
  • 4,167
  • 2
  • 22
  • 23
  • Thanks for the answer. It is strange that the description of this variable in apple docs is still empty. – Kaunteya Aug 15 '20 at 14:11
  • Freaking apple, even something as easy as this is completely hidden from plain sight. Seems like they want to drop AppKit as soon as possible or something. – Tj3n Nov 08 '21 at 09:27
5

If you use plain text in NSTextView (because you need the scrollview, for example), simply bind to the value property instead of attributedString. This binding will use the text color setting of the control, and works perfectly with Dark Mode.

Ely
  • 8,259
  • 1
  • 54
  • 67
  • I'm new to binding, but I don't see `value` in the properties panel. I tried `Value Path` and it didn't work. – Mr Rogers Apr 16 '19 at 18:05
  • Binding to the "value" does not work to correctly change the color in Dark Mode, though the text correctly shows up. [[self commentsView] bind:@"value" toObject:[[[self document] windowController] itemsController] withKeyPath:@"selection.userComments" options:valueOptions]; – Trygve Aug 12 '19 at 17:03
2

NSTextView extension for "plain" (non-attributed) strings. Works in either light or dark mode:

extension NSTextView {
    static let DefaultAttribute =
        [NSAttributedString.Key.foregroundColor: NSColor.textColor] as [NSAttributedString.Key: Any]

    var stringValue: String {
        return textStorage?.string ?? ""
    }

    func setString(_ string: String) {
        textStorage?.mutableString.setString("")
        append(string)
    }

    func append(_ string: String) {
        let attributedText = NSAttributedString(string: string, attributes: NSTextView.DefaultAttribute)
        textStorage?.append(attributedText)
    }
}
ToddX61
  • 683
  • 6
  • 12