如何在NSAttributedString中替换NSColor适配旧版macOS深色模式?
我之前在适配Mojave之前的macOS深色模式时,也踩过逐个遍历属性导致卡顿的坑,尤其是多文档同时打开的时候,简直没法用!下面给你几个经过实践验证的高效方案,涵盖Swift和Objective-C两种写法:
方案1:绘制时动态替换颜色(最优,无需修改原字符串)
这个方法的核心是在NSTextView绘制文本的环节拦截颜色属性,不用修改原有的NSAttributedString,性能拉满,特别适合多文档场景。
原理是利用NSLayoutManagerDelegate的回调,在绘制每个glyph之前临时替换颜色属性:
Swift 实现:
- 给你的NSTextView子类(或控制器)设置为layoutManager的代理:
textView.layoutManager.delegate = self
- 实现
NSLayoutManagerDelegate的代理方法:
extension YourTextViewClass: NSLayoutManagerDelegate { func layoutManager(_ layoutManager: NSLayoutManager, shouldUseTemporaryAttributes temporaryAttributes: inout [NSAttributedString.Key : Any], forDrawingToScreen toScreen: Bool, withAttributes attributes: [NSAttributedString.Key : Any], at characterIndex: Int) { // 只在深色模式下替换颜色(需自行实现外观模式判断逻辑) guard isDarkModeEnabled else { return } // 获取原文本颜色并映射为深色模式对应色 if let originalColor = attributes[.foregroundColor] as? NSColor { let mappedColor = mapOriginalColorToDarkMode(originalColor) temporaryAttributes[.foregroundColor] = mappedColor } } // 自定义颜色映射规则,可根据需求扩展 private func mapOriginalColorToDarkMode(_ color: NSColor) -> NSColor { if color == NSColor.textColor { return NSColor.white } else if color == NSColor.gray { return NSColor.lightGray } return color } }
Objective-C 实现:
- 设置代理:
textView.layoutManager.delegate = self;
- 实现代理方法:
@interface YourTextViewClass () <NSLayoutManagerDelegate> @end @implementation YourTextViewClass - (void)layoutManager:(NSLayoutManager *)layoutManager shouldUseTemporaryAttributes:(NSMutableDictionary<NSAttributedStringKey, id> * _Nonnull __autoreleasing *)temporaryAttributes forDrawingToScreen:(BOOL)toScreen withAttributes:(NSDictionary<NSAttributedStringKey, id> * _Nonnull)attributes at:(NSUInteger)characterIndex { if (!self.isDarkModeEnabled) return; NSColor *originalColor = attributes[NSForegroundColorAttributeName]; if (originalColor) { NSColor *mappedColor = [self mapOriginalColorToDarkMode:originalColor]; (*temporaryAttributes)[NSForegroundColorAttributeName] = mappedColor; } } - (NSColor *)mapOriginalColorToDarkMode:(NSColor *)color { if ([color isEqual:NSColor.textColor]) { return NSColor.whiteColor; } else if ([color isEqual:NSColor.grayColor]) { return NSColor.lightGrayColor; } return color; } @end
方案优势:
- 无需修改原NSAttributedString,完全在绘制层处理,内存占用低
- 多文档场景下每个视图独立处理,互不影响
- 切换模式时只需调用
textView.setNeedsDisplay()即可立即刷新,响应迅速
方案2:批量替换属性(适合需要修改原字符串的场景)
如果必须修改原NSAttributedString(比如需要保存修改后的内容),可以用更高效的批量遍历方式,避免逐个字符处理:
Swift 实现:
extension NSMutableAttributedString { func replaceTextColors(forDarkMode isDarkMode: Bool) { // 仅遍历包含前景色属性的范围,减少迭代次数 enumerateAttribute(.foregroundColor, in: NSRange(location: 0, length: length), options: []) { value, range, stop in guard let originalColor = value as? NSColor else { return } let newColor = isDarkMode ? mapToDarkColor(originalColor) : originalColor self.addAttribute(.foregroundColor, value: newColor, range: range) } } // 自定义颜色映射规则 private func mapToDarkColor(_ color: NSColor) -> NSColor { switch color { case NSColor.textColor: return .white case NSColor.systemBlue: return .systemLightBlue default: return color } } }
调用方式:
textView.textStorage?.replaceTextColors(forDarkMode: true)
Objective-C 实现:
@implementation NSMutableAttributedString (ColorReplacement) - (void)replaceTextColorsForDarkMode:(BOOL)isDarkMode { [self enumerateAttribute:NSForegroundColorAttributeName inRange:NSMakeRange(0, self.length) options:0 usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) { if ([value isKindOfClass:[NSColor class]]) { NSColor *originalColor = (NSColor *)value; NSColor *newColor = isDarkMode ? [self mapToDarkColor:originalColor] : originalColor; [self addAttribute:NSForegroundColorAttributeName value:newColor range:range]; } }]; } - (NSColor *)mapToDarkColor:(NSColor *)color { if ([color isEqual:NSColor.textColor]) { return NSColor.whiteColor; } else if ([color isEqual:NSColor.systemBlueColor]) { return NSColor.systemLightBlueColor; } return color; } @end
调用方式:
[textView.textStorage replaceTextColorsForDarkMode:YES];
优化技巧:
- 使用
enumerateAttribute(_:in:options:using:)而非遍历所有属性,仅处理包含目标属性的范围 - 提前缓存颜色映射结果,避免重复计算
- 将替换操作放到后台队列执行,完成后回到主线程更新textStorage,避免UI卡顿
方案3:自定义动态NSColor(提前适配,一劳永逸)
如果你的应用处于开发初期,或可修改文本创建逻辑,可以自定义支持动态切换的NSColor子类,后续无需修改字符串:
Swift 实现:
class DynamicColor: NSColor { private let lightColor: NSColor private let darkColor: NSColor init(light: NSColor, dark: NSColor) { self.lightColor = light self.darkColor = dark super.init(cgColor: light.cgColor)! } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func colorUsingColorSpaceName(_ colorSpaceName: NSColorSpaceName) -> NSColor? { return isDarkModeEnabled ? darkColor.colorUsingColorSpaceName(colorSpaceName) : lightColor.colorUsingColorSpaceName(colorSpaceName) } // 自行实现外观模式判断逻辑 private var isDarkModeEnabled: Bool { return UserDefaults.standard.bool(forKey: "DarkModeEnabled") } }
使用时直接创建动态颜色:
let textColor = DynamicColor(light: .black, dark: .white) let attributedString = NSAttributedString(string: "Hello", attributes: [.foregroundColor: textColor])
切换模式时只需发送通知,让所有视图刷新即可,无需修改字符串。
内容的提问来源于stack exchange,提问作者Antifonic
相关产品推荐
相关产品推荐

