如何通过NSLayoutManager过滤特定范围?NSTextView搜索实现咨询
嘿,我刚好折腾过类似的需求——用NSTextView替代卡成狗的NSTableView做大量文本展示,同时实现搜索高亮。结合你提到的NSTextStorage+NSLayoutManagerDelegate的思路,给你梳理下具体的实现步骤和关键代码,应该能直接复用!
核心思路
本质上就是利用NSTextView的文本存储层NSTextStorage(它是一个可变的富文本容器),给所有匹配搜索关键词的文本范围打上自定义属性标记;然后通过NSLayoutManager的代理方法,在文本绘制阶段识别这些标记,给对应区域加上高亮背景。这种方式比直接修改文本样式更灵活,而且不会破坏原文本的内容。
具体实现步骤
1. 监听搜索框的输入变化
首先得让搜索框的输入变化触发高亮更新逻辑,最简单的方式是给NSSearchField绑定动作:
// 在视图初始化或viewDidLoad里设置 searchField.target = self searchField.action = #selector(searchTextChanged(_:)) // 处理搜索文本变化的方法 @objc private func searchTextChanged(_ sender: NSSearchField) { let searchKeyword = sender.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) // 触发高亮更新 updateSearchHighlights(with: searchKeyword) }
2. 给匹配文本打自定义属性标记
接下来要遍历NSTextStorage里的所有文本,找到匹配关键词的范围,给这些范围加上自定义属性(比如我们定义一个叫SearchHighlight的属性键)。注意每次更新前要先清除旧的高亮标记,避免残留:
private func updateSearchHighlights(with keyword: String) { guard let textStorage = textView.textStorage else { return } // 先移除所有旧的高亮属性 let highlightKey = NSAttributedString.Key(rawValue: "SearchHighlight") textStorage.removeAttribute(highlightKey, range: NSRange(location: 0, length: textStorage.length)) // 如果搜索关键词为空,直接返回 guard !keyword.isEmpty else { return } // 设置匹配选项:大小写不敏感、忽略变音符号(可以根据需求调整) let matchOptions: NSString.CompareOptions = [.caseInsensitive, .diacriticInsensitive] let fullText = textStorage.string var currentSearchRange = NSRange(location: 0, length: fullText.count) var foundRange: NSRange // 循环查找所有匹配的范围 repeat { foundRange = (fullText as NSString).range(of: keyword, options: matchOptions, range: currentSearchRange) if foundRange.location != NSNotFound { // 给匹配范围打上高亮标记 textStorage.addAttribute(highlightKey, value: true, range: foundRange) // 更新搜索范围,继续查找下一个匹配 currentSearchRange = NSRange( location: foundRange.location + foundRange.length, length: fullText.count - (foundRange.location + foundRange.length) ) } } while foundRange.location != NSNotFound }
3. 通过LayoutManager代理绘制高亮背景
这是实现视觉高亮的关键一步。我们需要实现NSLayoutManagerDelegate的方法,在系统绘制文本背景之后,额外绘制高亮背景:
首先要给textView的layoutManager设置代理:
// 在视图初始化或viewDidLoad里设置 textView.layoutManager?.delegate = self
然后实现代理方法:
extension YourViewController: NSLayoutManagerDelegate { func layoutManager(_ layoutManager: NSLayoutManager, drawBackgroundForGlyphRange glyphsToShow: NSRange, at origin: CGPoint) { // 先让系统绘制默认的文本背景 layoutManager.defaultDrawBackground(forGlyphRange: glyphsToShow, at: origin) guard let textStorage = layoutManager.textStorage else { return } let highlightKey = NSAttributedString.Key(rawValue: "SearchHighlight") // 把当前要绘制的glyph范围转换成字符范围 let characterRange = layoutManager.characterRange(forGlyphRange: glyphsToShow, actualGlyphRange: nil) // 遍历所有带高亮标记的文本范围 textStorage.enumerateAttribute(highlightKey, in: characterRange, options: []) { value, range, _ in guard value != nil else { return } // 把字符范围再转回glyph范围,然后获取对应的绘制矩形 let glyphRange = layoutManager.glyphRange(forCharacterRange: range, actualCharacterRange: nil) let boundingRects = layoutManager.boundingRects(forGlyphRange: glyphRange, in: textView.textContainer!) // 设置高亮颜色(这里用半透明黄色,你可以换成自己需要的颜色) let highlightColor = NSColor.yellow.withAlphaComponent(0.3) highlightColor.setFill() // 给每个匹配区域绘制高亮背景 for rect in boundingRects { let drawRect = rect.offsetBy(dx: origin.x, dy: origin.y) NSBezierPath(rect: drawRect).fill() } } } }
一些优化小技巧
- 防抖处理:如果文本特别大,用户快速输入搜索关键词时会频繁触发更新,可以给
searchTextChanged加个防抖延迟,比如用DispatchQueue.main.asyncAfter(deadline: .now() + 0.1),避免频繁计算。 - 异步预匹配:极端大文本场景下,可以把匹配逻辑放到后台线程(但注意NSTextStorage的修改必须在主线程,所以后台只做范围查找,找到后再回到主线程更新属性)。
- 动态文本适配:如果你的NSTextView是实时更新内容的,记得在文本追加或修改后调用
updateSearchHighlights重新高亮。
内容的提问来源于stack exchange,提问作者Steve Mills
相关产品推荐
相关产品推荐

