You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在NSAttributedString中替换NSColor适配旧版macOS深色模式?

我之前在适配Mojave之前的macOS深色模式时,也踩过逐个遍历属性导致卡顿的坑,尤其是多文档同时打开的时候,简直没法用!下面给你几个经过实践验证的高效方案,涵盖Swift和Objective-C两种写法:

方案1:绘制时动态替换颜色(最优,无需修改原字符串)

这个方法的核心是在NSTextView绘制文本的环节拦截颜色属性,不用修改原有的NSAttributedString,性能拉满,特别适合多文档场景。

原理是利用NSLayoutManagerDelegate的回调,在绘制每个glyph之前临时替换颜色属性:

Swift 实现:

  1. 给你的NSTextView子类(或控制器)设置为layoutManager的代理:
textView.layoutManager.delegate = self
  1. 实现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 实现:

  1. 设置代理:
textView.layoutManager.delegate = self;
  1. 实现代理方法:
@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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 07:57:53