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

如何保存主从绑定NSTableView的选中状态?

macOS绑定中分类切换时保存/恢复选中状态的问题

我有一个数据管理器类,通过两个属性配置:一个用于设置分类,另一个用于选择该分类对应的项,以此暴露相关数据。我使用了多种选择形式,包括一对IndexSet。

我的核心需求是保存每个分类对应的选中项,切换分类时自动恢复之前的选中状态。纯代码实现这个逻辑很简单,但在macOS应用中使用绑定配置时却遇到了问题:切换分类时,绑定到选中状态的对象会在分类实际更新前清空或“保留”选中项,导致实际选中状态被覆盖,且无法区分这种行为是系统触发还是用户操作。


测试代码(展示覆盖行为)

以下代码生成了随机测试数据,未实现保存/恢复逻辑,仅用于复现问题:

class Thing: NSObject {
    @objc dynamic var name: String

    required init(name: String) {
        self.name = name
    }
}

class Stuff: NSObject {
    @objc dynamic var name: String
    @objc dynamic var things: [Thing]

    required init(name: String, things: [Thing]) {
        self.name = name
        self.things = things
    }
}

class StuffManager: NSObject {

    @objc dynamic var stuff = [Stuff]()
    @objc dynamic var stuffIndex = IndexSet() {
        didSet {
            print("STUFF: ", Array(stuffIndex))
        }
    }

    @objc dynamic var things = [Thing]()
    @objc dynamic var thingsIndex = IndexSet() {
        didSet {
            print("THING: ", Array(thingsIndex))
        }
    }

}

class ViewController: NSViewController {

    @objc dynamic var stuffManager = StuffManager()

    override func viewDidLoad() {
        super.viewDidLoad()
        (1...10).forEach { stuffManager.things.append(Thing(name: "Thing \($0)")) }
        (1...9).forEach {
            let randomThings = Array(stuffManager.things.shuffled()[0...Int.random(in: 0..<10)])
            stuffManager.stuff.append(Stuff(name: "Collection \($0)", things: randomThings))
        }
        stuffManager.stuff.append(Stuff(name: "Collection 10", things: []))
    }

}

Interface Builder绑定设置

我创建了包含NSPopButton(选择Stuff)、多选NSTableView(选择Things)及对应NSArrayController的视图,绑定配置如下:

Stuff数组控制器

  • 内容数组:绑定到ViewController,模型键路径stuffManager.stuff
  • 选中索引:绑定到ViewController,模型键路径stuffManager.stuffIndex

Things数组控制器

  • 内容数组:绑定到Stuff数组控制器,控制器键Selection,模型键路径things
  • 选中索引:绑定到ViewController,模型键路径stuffManager.thingIndex

界面控件按标准方式绑定:内容绑定到arrangedObjects,选中索引绑定到对应数组控制器的selectionIndexes。


问题细节

测试时发现,切换弹出按钮的分类时,THING调试日志会先于STUFF日志输出——也就是Thing的选中状态在Stuff分类更新前就被修改。这导致stuffManager.things属性观察器中保存选中状态的代码,会在知晓分类已变更前,保存无效的系统触发状态,而非用户实际选择的状态。

这种系统行为是为了避免内容变化导致选中状态无效或越界,但带来的问题是:

  • 无法区分系统自动触发的选中状态变更和用户主动操作
  • 仅在选中状态会变化时触发该行为(比如选中项不在新分类中),阻碍了缓存逻辑(无法判断何时需要保存之前的有效状态)

尝试过的无效方案

我曾尝试给每个Stuff单独添加IndexSet属性存储选中状态:

@objc dynamic var selected = IndexSet() {
    didSet {
        print("THING: ", Array(selected))
    }
}

并将Things数组控制器的选中索引绑定改为:

绑定到Stuff数组控制器,控制器键selection,模型键路径selected

但测试后发现这种方式不可行——TableView无法感知选中索引绑定的动态变化。


可行解决思路

放弃系统默认的主从绑定模式,在数据管理器中维护独立的[Thing]属性,手动处理分类切换时的内容更新与选中状态恢复。

示例修改后的StuffManager代码:

class StuffManager: NSObject {

    @objc dynamic var stuff = [Stuff]()
    @objc dynamic var stuffIndex = IndexSet() {
        didSet {
            print("STUFF: ", Array(stuffIndex))
            // 1. 保存旧分类的选中状态
            if let oldStuffIndex = oldValue.first, oldStuffIndex < stuff.count {
                let oldStuff = stuff[oldStuffIndex]
                savedSelections[oldStuff] = thingsIndex
            }
            // 2. 更新当前显示的内容
            if let newStuffIndex = stuffIndex.first, newStuffIndex < stuff.count {
                availableThings = stuff[newStuffIndex].things
                // 3. 恢复新分类的选中状态
                let newStuff = stuff[newStuffIndex]
                thingsIndex = savedSelections[newStuff] ?? IndexSet()
            }
        }
    }

    @objc dynamic var availableThings = [Thing]()
    @objc dynamic var thingsIndex = IndexSet() {
        didSet {
            print("THING: ", Array(thingsIndex))
        }
    }
    
    // 缓存每个分类的选中状态
    private var savedSelections: [Stuff: IndexSet] = [:]
}

将表格内容绑定到availableThings,这样就能完全控制分类切换时的选中状态保存与恢复流程,不受系统绑定的自动行为干扰。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.04 04:40:36