防止 NSTableView 在用户填充值之前保存新行

Prevent NSTableView from saving new row until user fills values

我正在为我的应用开发首选项 window,用户可以在其中使用 NSTableView 添加和删除预设。预设定义为:

struct Preset: Codable {
    var name: String
    var value: Int
}

这些预设应该保留到 UserDefaults。我的目标是尽可能少写代码,少写的应该是通用代码,所以我的项目使用了NSUserDefaultsControllerNSArrayController和Cocoa绑定。鉴于数组、字符串和整数类型在 UserDefaults 使用的 属性 列表中可用以存储这些设置,我决定使用这些而不是不透明的序列化数据类型。

此外,为了便于其余代码访问这些预设,我创建了一个带有 属性 presets 类型 [Preset] 的设置单例对象,它实现了计算 getters 和设置器从 UserDefaults 中检索它。

我已经将 MCVE 上传到 GitHub。它每秒记录 presets 属性 的值,以证明我需要帮助解决的问题——除了这两个问题,我相信一切正常。

第一个问题是我想在我的 NSTableView 中转到基于视图的内容模式,但是当我进行此更改并恢复另一个时,对 NSTableView 没有保存到 UserDefaults。它们在基于单元格的内容模式下工作良好,这是我在 MCVE 中使用的模式。由于我是 Cocoa/Swift 新手,我非常感谢有关此项目中需要更改哪些内容的分步说明,以便迁移到基于视图的内容模式,同时保留编辑条目的功能NSTableView.

编辑:看起来我在这个问题上是一个人:请参阅 Cocoa 邮件列表中的 this project, as well as this 主题。

然而,第二个问题是最严重的:当我按下 + 按钮向我的 NSTableView 添加新行时,最初 "name" 和 "value" 列为空,直到用户向其添加一些 text/values。但是,绑定将一个新的空行(没有 namevalue 属性)保存到 UserDefaults。这可以通过注释掉 printPresets() 中的代码看出。例如,在添加新行之前,这是 plist 的内容:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>presets</key>
    <array>
        <dict>
            <key>name</key>
            <string>First preset</string>
            <key>value</key>
            <integer>1</integer>
        </dict>
        <dict>
            <key>name</key>
            <string>Second preset</string>
            <key>value</key>
            <integer>2</integer>
        </dict>
    </array>
</dict>
</plist>

按下+后,这是plist的新内容:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>presets</key>
    <array>
        <dict>
            <key>name</key>
            <string>First preset</string>
            <key>value</key>
            <integer>1</integer>
        </dict>
        <dict>
            <key>name</key>
            <string>Second preset</string>
            <key>value</key>
            <integer>2</integer>
        </dict>
        <dict/>
    </array>
</dict>
</plist>

注意第二个预设后的额外 <dict/>

如果 getter 在设置处于此状态时运行,并且有一个空行,getter 会尝试从 plist 中读取此行,但由于缺少 namevalue 属性。具体来说,我收到以下错误:

Thread 1: Fatal error: 'try!' expression unexpectedly raised an error:
Swift.DecodingError.keyNotFound(CodingKeys(stringValue: "name",
intValue: nil), Swift.DecodingError.Context(codingPath: 
[_PlistKey(stringValue: "Index 2", intValue: 2)], debugDescription:
"No value associated with key CodingKeys(stringValue: \"name\",
intValue: nil) (\"name\").", underlyingError: nil))

我不知道如何解决这个问题。我试过一件事,但没有成功,然后想到了另一件事,我可以就如何实施提出一些建议。如果有人建议我没有想到的其他替代方案,我也将不胜感激。

  1. 我尝试将 sharedUserDefaultsController.appliesImmediately 设置为 false,添加一个 "Save" 按钮并将其绑定到 sharedUserDefaultsControllersave:行动。当我这样做时,我的预设没有加载。另一方面,我可以使用 + 按钮添加新行,而不会立即使应用程序崩溃。只要在按下 "Save" 按钮之前向该行添加名称和值,应用程序就不会崩溃。但是,如果我尝试在不填写名称和/或值字段的情况下进行保存,应用程序仍然会崩溃。所以,不是一个好的解决方案。也许我可以将 "Save" 按钮变灰,除非字段已正确填写?但是话又说回来,如果都一样,我想为此使用 "save-less" 范式 window.

  2. 我看到的另一种可能性是在创建新行时用一些默认数据填充 "name" 和 "value" 字段,这在某种程度上是合理的。我找到了 "Placeholder" 属性,但它不合适:它只是给用户的一个建议,直到用户真正填写一些东西,这些字段都是留空的,不会写入 plist。

我通过将 NSArrayController 的属性检查器中 "Object Controller" 下的 "Class Name" 更改为我的 MyModuleName.Preset 来解决这个问题,现在声明为 class:

class Preset: NSObject, Codable {
    let name: String
    let value: Int

    override init() {
        self.name = "A preset"
        self.setpoint = 1
        super.init()
    }

    init(name: String, value: Int) {
        self.name = name
        self.value = value
        super.init()
    }
}

通过在 init()(没有参数的版本)中提供默认值,当创建新行时,它会填充这些默认值,从而使问题消失。