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

Combine框架下监听NSAttributedString属性变更的最佳方案

更优实现方案

你不需要手动封装重写NSMutableAttributedString的所有修改方法,利用类簇的底层调用特性即可实现零遗漏的变更传递,维护成本极低。


核心原理

NSMutableAttributedString属于类簇设计,所有上层修改API(包括addAttribute、addAttributes、removeAttribute、文本插入/删除/替换、富文本拼接等)最终都会落到两个核心底层方法上执行实际修改:

  • 文本内容变更入口:replaceCharacters(in:with:)
  • 属性变更入口:setAttributes(_:range:)

你只需要子类化NSMutableAttributedString重写这两个方法,插入变更通知逻辑,即可覆盖所有可能的修改操作,无需逐个实现上层API。


具体实现

第一步:创建可监听变更的富文本子类

class ObservableMutableAttributedString: NSMutableAttributedString {
    /// 变更回调,参数为本次修改影响的范围
    var onChange: ((_ affectedRange: NSRange) -> Void)?
    
    override init(string: String) {
        super.init(string: string)
    }
    
    override init(attributedString attrStr: NSAttributedString) {
        super.init(attributedString: attrStr)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func replaceCharacters(in range: NSRange, with str: String) {
        let replacementLength = (str as NSString).length
        let affectedRange = NSRange(location: range.location, length: replacementLength)
        super.replaceCharacters(in: range, with: str)
        onChange?(affectedRange)
    }
    
    override func setAttributes(_ attrs: [NSAttributedString.Key : Any]?, range: NSRange) {
        super.setAttributes(attrs, range: range)
        onChange?(range)
    }
}

注:哪怕是通过mutableString属性直接修改文本内容、调用系统后续新增的富文本修改API,最终都会走到上述两个重写方法,不会出现变更漏触发的问题。

第二步:简化MyString封装

去掉原有重复的属性修改封装方法,直接绑定变更回调自动刷新属性列表即可,同时修复原有代码中range计算的越界bug:

typealias NSAttrStringAttr = [NSAttributedString.Key: Any]
typealias MyStringAttribute = (NSAttrStringAttr, NSRange)

class MyString: ObservableObject {
    let attributedString: ObservableMutableAttributedString
    @Published var attributes: [MyStringAttribute] = []
    
    init(string: String) {
        let attrStr = ObservableMutableAttributedString(string: string)
        self.attributedString = attrStr
        
        // 任意变更触发后自动刷新属性列表
        attrStr.onChange = { [weak self] _ in
            self?.refreshAttributes()
        }
        refreshAttributes()
    }
    
    private func refreshAttributes() {
        attributes.removeAll()
        let totalLength = attributedString.length
        guard totalLength > 0 else { return }
        let fullRange = NSRange(location: 0, length: totalLength)
        attributedString.enumerateAttributes(in: fullRange, options: []) { attrs, range, _ in
            attributes.append((attrs, range))
        }
    }
}

第三步:优化MyStringGroup刷新逻辑

原有订阅逻辑无需大改,收到子MyString的变更通知后,可根据业务场景选择刷新策略:

  • 子串数量少、总文本长度短:直接重新拼接所有子串生成全量富文本,逻辑简单无额外计算成本
  • 超长文本、子串数量多:记录每个MyString在总富文本中的起始偏移量,收到变更时仅替换对应范围的片段,实现增量刷新
class MyStringGroup {
    private var strings: [MyString] = []
    private var _attributedString: NSMutableAttributedString?
    var cancellable = Set<AnyCancellable>()
    
    var attributedString: NSMutableAttributedString {
        get {
            if let str = _attributedString {
                return str
            }
            let newAString = NSMutableAttributedString(string: "")
            strings.forEach { newAString.append($0.attributedString) }
            _attributedString = newAString
            return newAString
        }
    }
    
    func append(_ string: MyString) {
        // 订阅子串变更
        string.$attributes.sink(receiveValue: { [weak self] _ in
            // 标记缓存失效,下次读取时自动重新生成;或做增量替换
            self?._attributedString = nil
        }).store(in: &cancellable)
        strings.append(string)
        // 新增子串后清空缓存
        _attributedString = nil
    }
}

备选轻量方案

如果你的业务和TextKit渲染链路绑定,可以直接使用NSTextStorage(系统提供的NSMutableAttributedString子类),它本身自带编辑周期通知,监听NSTextStorage.didProcessEditingNotification即可拿到每次变更的范围和类型,无需自行子类化。如果只是纯数据层的富文本处理,上述子类化方案更轻量无依赖。

内容的提问来源于stack exchange,提问作者Peter R

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 04:03:26