(Note: full project for experiments may be found here: https://github.com/snechaev/cocoa-validation-question)
I have a simple two-column view-based NSTableView
connected to the data using the Cocoa bindings via the `ArrayController:
WindowController
ArrayController
's Content Array
is binded to the File owner's->Data
TextField
) is binded to the Table Cell View -> objectValue.Value
. The "Validates immediately" option is turned on.
The data model for the table is as follows
//Data is a property of WindowController
@objc let Data : NSMutableArray = NSMutableArray(array: [TestClass(Name: "1"),
TestClass(Name: "2"),
TestClass(Name: "3"),
TestClass(Name: "4"),
TestClass(Name: "5")])
class TestClass: NSObject {
@objc let Name : String!
@objc var Value : String?
init(Name: String!) {
self.Name = Name
self.Value = Name
}
override func validateValue(_ ioValue: AutoreleasingUnsafeMutablePointer<AnyObject?>,
forKey inKey: String) throws {
if(inKey == #keyPath(Value)){
guard let strVal = ioValue.pointee as? String
else {throw MyError.error("Value should be a string")}
if(!strVal.starts(with: "1")){
throw MyError.error("Value should starts with 1");
}
}
}
}
The model implements validation for the Value
parameter values using the validateValue(_:forKey:)
override. The validation works fine except for the following case:
The result is that the edited cell has lost the focus, but remains with the invalid value. In addition, when the user returns to edit mode for this cell and presses Enter, no error message is displayed. And the cell will still remains with the invalid value, so the user may think that this value is fully valid. If we inspect the data model we will clearly see that the invalid value was not assigned in the corresponding TestClass instance (which is ok).
So the question is how to handle such a situation so as not to mislead the user? It seems to me that the best way is to restore the initial value when Esc is pressed, but I can't find a way to do this. And maybe Apple's guidelines advise the other behavior for this situation?
It looks like a bug in the view based NSTableView
. The cell based NSTableView
and a NSTextField
outside a table view do nothing when Esc is pressed and beep. Validating the data in a formatter has a similar issue.
Workaround 1 in a NSTextField
subclass:
class TextField: NSTextField {
override func abortEditing() -> Bool {
// bugfix, the data isn't restored after an error
let aborted = super.abortEditing()
// restore the data
if aborted,
let bindingInfo = infoForBinding(.value),
let object = bindingInfo[.observedObject] as? NSObject,
let keyPath = bindingInfo[.observedKeyPath] as? String {
objectValue = object.value(forKeyPath: keyPath)
}
return aborted
}
}
Workaround 2 in a NSTableView
subclass:
class TableView: NSTableView {
override func cancelOperation(_ sender: Any?) {
// bugfix, the data isn't restored after an error
// get the edited control
var control: NSControl?
if let firstResponder = window?.firstResponder {
if let fieldEditor = firstResponder as? NSTextView,
let delegate = fieldEditor.delegate as? NSControl {
control = delegate
}
else if let firstResponder = firstResponder as? NSControl {
control = firstResponder
}
}
super.cancelOperation(sender)
// restore the data
if let control = control,
let bindingInfo = control.infoForBinding(.value),
let object = bindingInfo[.observedObject] as? NSObject,
let keyPath = bindingInfo[.observedKeyPath] as? String {
control.objectValue = object.value(forKeyPath: keyPath)
}
}
}
And maybe Apple's guidelines advise the other behavior for this situation?
The outline views in Finder and Xcode always discard the change if it's invalid. I don't know how to do this and I think it's more user-friendly to be able to fix a typo.