问题
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.
I tried setting
sharedUserDefaultsController.appliesImmediately
tofalse
, adding a "Save" button and binding it to thesharedUserDefaultsController
'ssave:
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.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.
回答1:
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.
来源:https://stackoverflow.com/questions/54385105/prevent-nstableview-from-saving-new-row-until-user-fills-values