且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

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

更新时间:2023-12-03 17:10:52

我通过在的属性检查器中的对象控制器下更改类名来解决此问题code> NSArrayController 到我的 MyModuleName.Preset 中,现在声明为类:

  class预设:NSObject,可编码{
let名称:字符串
let值:Int

覆盖init(){
self.name =一个预置
self.setpoint = 1
super.init()
}

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

通过在 init()(不带参数的版本)中提供默认值,当出现新行时是cre吃完后,它会填充这些默认值,从而使问题消除。


I'm working on a preferences window for my app, where the user can add and remove presets using NSTableView. A preset is defined as:

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

These presets should be persisted to UserDefaults. My goal is to write as little code as possible, and what little is written should be generic code, so my project makes use of NSUserDefaultsController, NSArrayController and Cocoa Bindings. Seeing as array, string and integer types are available in the property lists used by UserDefaults to store these settings, I've decided to use these rather than opaque serialized data types.

Also, to facilitate access to these presets by the rest of the code, I created a Settings singleton object with a property presets of type [Preset] which implements computed getters and setters to retrieve this from UserDefaults.

I've uploaded an MCVE to GitHub. It logs the value of the presets property every second to demonstrate the issues I need help with -- other than these two issues, I believe everything is working fine.

The first issue is that I'd like to move to a view-based content mode in my NSTableView, but when I make this change and restore the other , any edits made to entries of the NSTableView are not saved to UserDefaults. They work fine in cell-based content mode, which is what I'm using in the MCVE. Since I'm a Cocoa/Swift newbie, I'd really appreciate step-by-step instructions about what needs to be changed in this project in order to migrate to view-based content mode, while keeping the functionality of editing entries of the NSTableView.

EDIT: Looks like I'm note alone in this issue: see this project, as well as this thread in a Cocoa mailing list.

However, the second issue is the most serious one: when I press the + button to add a new row to my NSTableView, initially the "name" and "value" columns are empty until the user adds some text/values to it. However, the bindings save a new empty row (without name and value properties) to UserDefaults. This can be seen by commenting out the code in printPresets(). For instance, before adding a new row, this is the content of the 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>

After pressing +, this is the new content of the 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>

Note the extra <dict/> after the second preset.

If the getter runs while the settings are in this state, with an empty row, the getter tries to read in this row from the plist and fails due to the lack of name and value properties. Specifically, I get the following error:

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))

I have no idea how to fix this. I've tried one thing, unsuccesfully, and thought up of another, which I could use some advice on how to implement. I'd also appreciate if other alternatives that I didn't think of were suggested.

  1. I tried setting sharedUserDefaultsController.appliesImmediately to false, adding a "Save" button and binding it to the sharedUserDefaultsController's save: action. When I do this, my presets aren't loaded. On the other hand, I can add a new row with the + button without immediately crashing the app. As long as I add a name and a value to this row before pressing the "Save" button, the app doesn't crash at all. However, if I try to save without filling in either the name or the value fields, or both, the app still crashes. So, not a good solution. Perhaps if I could gray out the "Save" button unless the fields were properly filled? But then again, if it's all the same, I'd like to use a "save-less" paradigm for this window.

  2. Another possibility that I see, and which is somewhat reasonable, would be to fill the "name" and "value" fields with some default data when a new row is created. I found the "Placeholder" attribute, but it's not suitable: it's just a suggestion for the user, and until the user actually fills in something, the fields are left blank and not written to the plist.

I solved this by changing the "Class Name" under "Object Controller" in the Attributes Inspector for NSArrayController to my MyModuleName.Preset, now declared as a 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()
    }
}

By providing defaults values in init() (the version without arguments), when a new row is created, it is filled with these default values, making the problem go away.