如何保存主从绑定NSTableView的选中状态?
我有一个数据管理器类,通过两个属性配置:一个用于设置分类,另一个用于选择该分类对应的项,以此暴露相关数据。我使用了多种选择形式,包括一对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

