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
相关产品推荐
相关产品推荐

