How to update an NSTextField programatically and have value propagate through binding - macos

I have a case where I would like to programmatically assign the first responder to an NSTextField and then programmatically insert a value into the NSTextField as well.
I have this working with the following code:
field.window?.makeFirstResponder(field)
field.stringValue = "Auto generated value"
In the example above, field has a binding to my model. If the user chooses to edit Auto generated value and then hit the return key, the first responder is resigned and the edited value is set on my model. BUT, if the user chooses not to edit Auto generated value and hits the return key, the first responder is resigned but the model is not updated.
It's as if the field is not marked as dirty when programmatically updating the value.
I would like to avoid having to manually update the model when the case above occurs due to some complexities that I have left out in the example. I would like it to apply through whichever binding is set just like it would when the user manually types in the value.
Is this possible?

If you use binding with a NSTextField or any other AppKit control, you should perform programmatically value changes on the data source / model, instead of the other way around by manipulating the control value in code. By doing so, you will not have the problems as described in your question.

Inserting the text in the field editor starts an editing session (undocumented feature).
if window.makeFirstResponder(textField1),
let fieldEditor = textField1.currentEditor() as? NSTextView {
fieldEditor.insertText("Auto generated value", replacementRange: NSRange(location: 0, length: fieldEditor.textStorage?.length ?? 0))
}

Related

Tableview updates values in the twilight zone

I have followed this tutorial and successfully bind my NSTableView to a core data entity.
The table is a view based cell and is populated perfectly from the core data entity.
I have + and - buttons bind to the NSArrayController. When I press the plus button a new record is added to core data and appear on the table, when I select an entry and press the minus sign, that is removed from the database. For that matter I have override this code for the add/remove keys.
#IBAction override func add(_ sender: (Any)?) {
let newApp = self.newObject() as AnyObject
newApp.setValue("New Entry", forKey: "name")
self.addObject(newApp as Any)
}
#IBAction override func remove(_ sender: (Any)?) {
// Do certain stuff before removing all selected rows
self.remove(atArrangedObjectIndexes: self.tableView!.selectedRowIndexes)
}
I made the table view cells editable. When the plus sign button is pressed a new entry is created and appear on the table with the text "New Entry". This new entry creates a core data record. Let's call it record 1.
Now I want the user to edit the entry with the name they want.
So I select the new cell and press enter. The cell is now on edit mode.
I type the new name I want to that cell, that will be passed to the core data entity.
I type, for example, BONOBO and press ENTER.
This table must contain just unique names. So, as soon as ENTER is pressed and the cell ends editing, I want to check core data to see if the name BONOBO is already taken and if it is, reject the name and tell the user to provide a new name.
This is the problem: as soon as I press ENTER, record 1 changes its name instantly from New Entry to BONOBO, before I can check if the entry already exists on the database, meaning that any check I do, will always tell me that the record exists. In fact the record exists in memory because the context was not saved yet.
I am intercepting the enter press by setting the delegate of all cells to a class and using its delegate method controlTextDidEndEditing(_ obj: Notification).
I have also tried to set an action for the textfields but the problem is the same.
How do I intercept it before the core data change happens?
The trick here is to leverage Key-Value Coding's built-in validation methods. See Object Validation.
I've never done this with a managed object, but the process seems to be the same as with regular KVC validation. You want to implement a method with the name:
validate<Key>:error:
... Where 'Key' is the name of the parameter you're trying to validate. This takes in a pointer for the value you want to validate, and another for an NSError object. Inside the method you test whether the passed-in value is acceptable. You can return true to accept it, modify the value and return true to accept a revised version, or return false to reject it outright (modifying the error object to have something to send back to the user).

How to bind hidden property in a control when a integer equals some certain value in OSX?

I've got a RadioGroup with 3 cells. I want to hide some controls when the selected index in radio group is 1. That is:
[someControl setHidden: radioGroup.selectedIndex == 1];
I've got a lot of controls will show/hidden when radio group selection changed. Some might show when the selected index equals 0, some might show when equals 2.
I want it to be done by binding, not connect each control reference using outlet.
How to acheive that?
There are at least two ways of doing this, as binding hidden requires a Boolean balue:
Create a property that is of type BOOL and returns YES or NO based on your value comparison, then in your class use KVO to observe the original value and set the Boolean property inside of the KVO observer (this is required to make sure the object is updated at the right time)
Use bindings alone, but create a Value Transformer to transform each value you need into a BOOL as necessary to be interpreted correctly. There is an existing value transformer that changes YES to NO and vice-versa, but for other value transforms you will have to create these yourself, and there is no good way to parametrize them inside of the xib file.
The first solution is probably easier.

NSArrayController: How to programmatically clear selection?

Very simple question that's driving me crazy: What's the proper way to clear the selection of an NSArrayController programmatically?
I'm designing a view with the following components:
NSArrayController *controller1: Bound to an array of objects
NSPopUpView view1: Content bound to controller1.arrangedObjects; Value bound to controller1.selection; "Inserts Null Placeholder" selected
NSArrayController *controller2: Bound to an array stored in controller1.selection
NSPopupView view2: Content bound to controller2.arrangedObjects; value bound to controller2.selection; "Inserts Null Placeholder" selected
Initially, view1's content is populated; controller1 and controller2 have nil selection values; and view1 and view2 display null placeholders. Selection of controller1 causes controller1's selection to change and view2's content to populate. All good.
I'd like to implement a Clear button that clears the selection of controller1, which, thanks to bindings, should also clear the selection of controller2 and reset view1 and view2 to the null placeholder. For the life of me, I can't figure out the proper code for this very simple function. Altering the selection of controller1 fails to update the value shown in view1. Worse, altering the controller1 selection programatically causes weird things to happen in controller2: further selection of values in view1 fails to have any effect on view2.
Things I've tried:
Calling the SetSelectedObjects method of controller1 with an [NSArray new].
Calling the SetSelectedObjects method of controller1 with null.
Calling the SetSelectedIndex method of controller1 with NSNotFound.
Calling the RemoveSelectedIndex method of controller1 with the SelectedIndex property of controller1.
Looking in the Cocoa NSArrayController documentation for any class method or suggestion for clearing the selection value. Nothing there - not even any mention of this being desirable, let alone how to accomplish it.
Any ideas? Thanks...
According to Apples Developer documentation this can be done using setSelectionIndexes:
To deselect all indexes, pass an empty index set.
Objective-C:
[arrayController setSelectionIndexes:[NSIndexSet indexSet]];
Swift:
arrayController.setSelectionIndexes( NSIndexSet() )
Try controller1.selectionIndex = NSIntegerMax; and see if that works. I did a simple test with a label bound to the array controller's selection, and when I set the selectionIndex to NSIntegerMax, the no selection placeholder was displayed in the label.
As it turns out, the NSArrayController page does include a single recommendation for clearing the selection:
setSelectionIndexes:
Sets the receiver’s selection indexes and returns a Boolean value that indicates whether the selection changed.
(BOOL) setSelectionIndexes: (NSIndexSet *) indexes
Discussion
Attempting to change the selection may cause a commitEditing message which fails, thus denying the selection change.
To select all the receiver’s objects, indexes should be an index set with indexes [0...count -1]. To deselect all indexes, pass an empty index set.
However, I had actually tried that, and it still left me with a problem.
To generalize:
Create two classes, A and B, where A contains a property "NSArray *b_list" that contains a list of instances of B's.
Create an application with a property "NSArray *a_list". Populate it with some instances of A, and populate the b_list of each A instance with some B instances.
Create a window with two array controllers, Controller_A (bound to a_list) and Controller_B (bound to Controller_A.selection.b_list).
Create two pop-up buttons in the window, Popup_A (bound to Controller_A.arrangedObjects) and Popup_B (bound to Controller_B.arrangedObjects).
Create a "Clear" button with some logic to clear the selection of Controller_A. ("Some logic" is either the method recommended in the Apple documentation, or any other method.)
Run the application. Select an entry in Popup_A. Notice that Popup_B populates with the instances of Controller_A.selection.b_list, as it should.
Now hit the Clear button.
ERROR: Note that while the content and selection of Popup_A correctly become null, the same doesn't happen in Popup_B: it presents a blank entry with a single selected (blank) item. Note also that the selection properties of Controller_B indicates that there is a selection, but its properties are weird: selectedIndex points to 0 instead of NSNotFound, and selectedIndexes includes a non-empty selection integer range.
This is clearly an error with the binding logic that can definitely cause some exceptions and logical errors. For example, if there's any sort of binding attached to B_controller.selection, clearing A_controller will raise an exception relating to the selection value in B_controller, since it indicates a selection but points to garbage.
My workaround is not to bind anything directly to the B_controller selection. Instead, access B_controller programmatically, and in view of the selection value of A_controller, like this:
// setting some property c to the value of b_controller:
if ((B_controller.selectedIndex == NSNotFound) || A_controller.selectedIndex == NSNotFound))
c = nil;
else
c = [B_controller objectAtIndex: [B_controller.selectedIndex]];
I'm also submitting a bug report to Apple with this info.
Answer to a very old question but still.
I just needed the same thing, and for me this worked:
[arrayController setAvoidsEmptySelection:NO];
NSIndexSet *indexes = [NSIndexSet indexSetWithIndexesInRange:
NSMakeRange(NSNotFound, 0)];
[arrayController setSelectionIndexes:indexes];

How to bind objects in NSDictionary in NSUserDefaults?

How do I bind NSDictionary objects to several text fields in Interface Builder?
I would like to bind each object to a specific item in the dictionary. For example, the first text field should be bound to Actor:
You need to add a NSObjectController in Interface Builder. In the attributes inspector tab leave the standard mode "Class" and class name on "NSMutableDictionary", but switch "Prepares Content" on. In the bindings inspector bind "Content Object" to "Shared User Defaults Controller", controller key "values" and "Model Key Path" to your dictionary's key in User Defaults. Switch "Handles Content As Compound Value" on.
Now you can bind your text field (or check boxes etc). values to the ObjectController, with controller key "selection" (!) and the keys in your dictionary (as model key path).
Hope this helps. I couldn't find the answer in Apple's documentation and also not in the net, was lucky to find it out myself by trying different controller objects...
First: #keeluu's answer should be marked correct.
Doing it Programmatically
If you're doing this in code rather than through IB, there's a "gotcha" to look out for. It looks like this:
// Assume 'anObjectController' is an NSObjectController.
// Assume 'userDefaultsController' is [NSUserDefaultsController sharedUserDefaultsController]
// Assume 'someDictionary' is an NSDictionary in userDefaults.
[self.anObjectController
bind:NSContentBinding
toObject:userDefaultsController
withKeyPath:#"values.someDictionary"
options:#{NSHandlesContentAsCompoundValueBindingOption: #YES}];
If you do the above, you'll find that when you bind your UI elements to properties in someDictionary, those UI elements will correctly display the values that are IN user defaults, but when you change them (by say, clicking a bound checkbox) those values will NOT be updated in user defaults; the old values stick around.
The Reason
The binding must be to NSContentObjectBinding instead of NSContentBinding. The NSObjectController class exposes a binding named contentObject. When you bind to that, everything works properly.
This is an easy mistake to make because Xcode's code completion automatically goes for NSContentBinding and it's one of the most frequently used binding names anyway. This just cost me hours of trouble, so hopefully I saved someone else some time.
Try to add NSDictionaryController to you .xib file, then bind its Controller Content to your dictionary in User Defaults. Then you can bind text field values to Dictionary Controller (arrangedObjects key).
I never tried that but I think it must work.
Hope, it helps.

Enabling NSButton with bindings, based on NSTableView selection

I have a NSWindow containing an NSButton and an NSTableView.
I'd like the button to be enabled if and only if the table contains at least one item, and exactly one item is selected. (The table does not allow multiple selection.)
What can I bind the button's enabled binding to to make this happen?
This is an old thread, but here are my 2 cents:
Use and array controller and bind the button's enabled state to
Controller Key: selectedObjects
Model Key Path: #count
Works fine.
Try binding to the array controller's selectedObjects, model key path count, with no value transformer.
Note that this would be unsafe if you allowed multiple selection: For one thing, the count could easily be neither YES nor NO; for another, if the user selected a multiple of 256 items, the lowest byte of the count would be 0, so the BOOL value would be NO even though there is a selection.
I ran into this today and I got it to work after some efforts.
My button should be disabled if nothing is selected in the "Master Table":
Problems I ran into:
Use the actual button and not the enclosed Button cell
Specify NO = disabled for Multiple values, No selection etc.
Bind the Enabled property to the Master Table's selection and use a property (code in my case), which is present.
Use the transformer NSIsNotNil to enable the button if something is selected in the master table.

Resources