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

如何在NSOutlineView中集成使用NSTextFinder实现文本查找

NSTextFinder 适配NSOutlineView的无替换纯查找实现

当前配置的核心缺口

你已经完成了查找栏弹出、容器绑定、置灰效果的UI链路搭建,当前缺失的核心环节是:作为NSTextFinder.client的视图控制器,没有实现NSTextFinderClient协议的必要方法,因此NSTextFinder无法感知到outlineView内的可查找内容,自然无法触发匹配逻辑。

落地实现步骤

  • 让当前ViewController正式遵守NSTextFinderClient协议
  • 因为不需要替换能力,所有替换相关的协议方法直接留空即可,不需要额外实现复杂逻辑
  • 自行维护行级别的匹配结果数组,不需要侵入outlineView内部的单元格文本视图

关键逻辑说明

  1. 基础协议属性配置
    • allowsMultipleSelection根据outlineView的多选能力返回值,纯查找场景直接返回false即可
    • isSelectable固定返回true,支持查找定位时的行选中
    • allowsFindIndicator固定返回true,实现和Safari一致的系统匹配高亮效果
  2. 匹配逻辑实现
    • 遍历outlineView所有行,逐行取单元格的显示文本,和查找栏输入的文本做不区分大小写的包含匹配
    • 把所有命中的行号存入自定义的匹配结果数组,作为查找跳转的索引依据
  3. 定位跳转逻辑
    • 实现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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.25 04:15:03