如何在NSOutlineView中集成使用NSTextFinder实现文本查找
NSTextFinder 适配NSOutlineView的无替换纯查找实现
当前配置的核心缺口
你已经完成了查找栏弹出、容器绑定、置灰效果的UI链路搭建,当前缺失的核心环节是:作为NSTextFinder.client的视图控制器,没有实现NSTextFinderClient协议的必要方法,因此NSTextFinder无法感知到outlineView内的可查找内容,自然无法触发匹配逻辑。
落地实现步骤
- 让当前ViewController正式遵守
NSTextFinderClient协议 - 因为不需要替换能力,所有替换相关的协议方法直接留空即可,不需要额外实现复杂逻辑
- 自行维护行级别的匹配结果数组,不需要侵入outlineView内部的单元格文本视图
关键逻辑说明
- 基础协议属性配置
allowsMultipleSelection根据outlineView的多选能力返回值,纯查找场景直接返回false即可isSelectable固定返回true,支持查找定位时的行选中allowsFindIndicator固定返回true,实现和Safari一致的系统匹配高亮效果
- 匹配逻辑实现
- 遍历outlineView所有行,逐行取单元格的显示文本,和查找栏输入的文本做不区分大小写的包含匹配
- 把所有命中的行号存入自定义的匹配结果数组,作为查找跳转的索引依据
- 定位跳转逻辑
- 实现
scrollRangeToVisible(_:)协议方法,根据NSTextFinder传来的匹配索引,滚动到对应outlineView行,触发选中和查找指示器刷新
- 实现
补全后的参考代码
class ViewController: NSViewController, NSTextFinderClient { @IBOutlet var outlineView: NSOutlineView! @IBOutlet var textFinder: NSTextFinder! // 存储所有匹配到的outlineView行号 private var matchedRows: [Int] = [] // 记录当前定位到的匹配结果索引 private var currentMatchIndex: Int = 0 // MARK: NSTextFinderClient 必须实现的基础属性 var allowsMultipleSelection: Bool { false } var isSelectable: Bool { true } var allowsFindIndicator: Bool { true } var firstSelectedRange: NSRange { guard !matchedRows.isEmpty else { return .init() } return NSRange(location: currentMatchIndex, length: 1) } var selectedRanges: [NSValue] { return [NSValue(range: firstSelectedRange)] } // MARK: NSTextFinderClient 核心交互方法 func scrollRangeToVisible(_ range: NSRange) { // 校验索引合法性 let targetIndex = range.location guard targetIndex >= 0, targetIndex < matchedRows.count else { return } currentMatchIndex = targetIndex let targetRow = matchedRows[targetIndex] // 滚动并选中目标行 outlineView.scrollRowToVisible(targetRow) outlineView.selectRowIndexes(IndexSet(integer: targetRow), byExtendingSelection: false) // 刷新系统查找高亮 textFinder.updateFindIndicator(for: firstSelectedRange) } // 无替换需求,留空即可 func replace(withText string: String) {} // 搜索词变化时的匹配刷新逻辑 func refreshMatches(for searchKeyword: String) { matchedRows.removeAll() guard !searchKeyword.isEmpty else { textFinder.updateFindIndicator(for: .init()) return } // 遍历所有可见行做文本匹配 for row in 0..<outlineView.numberOfRows { guard let cellView = outlineView.view(atColumn: 0, row: row, makeIfNecessary: false) as? NSTableCellView, let displayText = cellView.textField?.stringValue else { continue } if displayText.localizedCaseInsensitiveContains(searchKeyword) { matchedRows.append(row) } } // 匹配完成后定位到第一个结果 currentMatchIndex = 0 if !matchedRows.isEmpty { scrollRangeToVisible(firstSelectedRange) } } @IBAction func findSomething(_ sender: Any?) { textFinder.performAction(.showFindInterface) } }
注意事项
- 不需要参考NSTextView场景下逐字符匹配的实现逻辑,NSOutlineView的查找只需要做到行级定位即可,完全可以实现和Safari一致的查找体验
- 当outlineView的数据源动态刷新时,主动调用一次匹配刷新方法,同步更新匹配结果
- 不需要自行写无匹配结果的提示UI,NSTextFinder会根据匹配结果数组的长度自动在查找栏展示提示
内容的提问来源于stack exchange,提问作者user10711707
相关产品推荐
相关产品推荐

