views:

2374

answers:

4

I'm trying to use an NSTextField for integer user input. The text field is bound to an NSNumber property and in the setter method I cleanup the input value (make sure it's an int) and set the property if necessary. I send the willChangeValueForKey: and didChangeValueForKey:, but the UI doesn't update to the new values while that text field is still active.

So for example I can type "12abc" in the text field that the setter method cleans up to "12", but the text field still shows "12abc".

I have "Continuously Update Value" checked in the interface builder.

(I've also noticed that the setter method receives an NSString, not an NSNumber. Is that normal?)

What's the correct way of hooking up an NSTextField to an NSNumber? What does the setter method look like for the property? How to prevent non-numeric values from showing up in the text field?

A: 

I think you need to set up a number formatter to your NSTextField.

Go read up about formatters on Apple's website: Applying Formatters.

Ben Alpert
I added an number formatter by dropping it on the text field as suggested in the doc you pointed out, but nothing happens. I can still type non numeric characters. Do I need to do anything other than connecting the formatter to the text field?
lajos
A: 

Like Ben mentioned, one way to do this is to attach an NSNumberFormatter to your textfield, which is pretty trivial to set up in interface builder and will likely Just Work™.

If you don't like the modal dialogs NSNumberFormatter throws at your users when they enter non-numeric values, you can subclass NSNumberFormatter to implement different, ‘more forgiving’ formatting behavior. I think overriding – numberFromString: to strip out non-numeric characters before calling super's implementation should do the trick.

Another approach is to create and register your own NSValueTransformer subclass to parse strings into NSNumbers and back, but I'd try using an NSNumberFormatter first since it's pretty clearly the class that has been designed for this exact purpose.

More about value transformers

Dirk Stoop
+8  A: 

I send the willChangeValueForKey: and didChangeValueForKey:, but the UI doesn't update to the new values while that text field is still active.

There are very few reasons to send those messages. Usually, you can do the same job better and more cleanly by implementing and using accessors (or, better yet, properties). KVO will send the notifications for you when you do that.

In your case, you want to either reject or filter bogus inputs (like “12abc”). The correct tool for this task is Key-Value Validation.

To enable this, check the “Validates Immediately” box on the binding in IB, and implement a validation method.

Filtering:

- (BOOL) validateMyValue:(inout NSString **)newValue error:(out NSError **)outError {
 NSString *salvagedNumericPart;
 //Determine whether you can salvage a numeric part from the string; in your example, that would be “12”, chopping off the “abc”.
 *newValue = salvagedNumericPart; //@"12"
 return (salvagedNumericPart != nil);
}

Rejecting:

- (BOOL) validateMyValue:(inout NSString **)newValue error:(out NSError **)outError {
 BOOL isEntirelyNumeric;
 //Determine whether the whole string (perhaps after stripping whitespace) is a number. If not, reject it outright.
 if (isEntirelyNumeric) {
  //The input was @"12", or it was @" 12 " or something and you stripped the whitespace from it, so *newValue is @"12".
  return YES;
 } else {
  if (outError) {
   *outError = [NSError errorWithDomain:NSCocoaErrorDomain code: NSKeyValueValidationError userInfo:nil];
  }
  //Note: No need to set *newValue here.
  return NO;
 }
}

(I've also noticed that the setter method receives an NSString, not an NSNumber. Is that normal?)

Yes, unless you use a value transformer that transforms strings into numbers, connect a number formatter to the formatter outlet, or substitute an NSNumber for the NSString in your validation method.

Peter Hosey
The validation methods seem to have a signature of:-(BOOL)validateNewValue:(id *)ioValue error:(NSError **)outError;But even if I implement a method with the correct signature and return NO or correct the value, the text field still shows non numeric characters.
lajos
Thanks for the note about the method signature—I've corrected my answer. Did you check the “Validates immediately” box? I'm not sure what else could be the problem, without seeing the code.
Peter Hosey
I posted a test project here: www.codza.com/files/numericFieldTest.zipThe validate method correctly returns YES or NO (see logs), but the text field still shows alpha characters. Thanks so much for looking at this. You can contact me at www.codza.com/contact and I'll also check the comments here.
lajos
Your instance variable doesn't need to be an IBOutlet. Other than that, the app works as expected for what you're doing, which is rejection. If you want filtering, then you need to scan a number from the string, and if that succeeds, replace the string with the number and return YES.
Peter Hosey
Thanks for looking at it. I tried replacing return NO; with *ioValue = (id)[intValue stringValue]; return YES;, but the alpha characters still show up in the text field while the user is typing. I understand that this method validates the value behind the scenes, but I'd like to prevent typing
lajos
characters while the textfield is active, too. Maybe that's just not how things work?
lajos
Ah, I see. For live validation like that, I think you would need to be the field's delegate and implement controlTextDidChange:, which is one of NSControl's delegate methods. If you determine the new text to be invalid, beep and change the control's (or field editor's?) text back to the old text.
Peter Hosey
Also, don't forget that you can call the model object's validation method from your controller's controlTextDidChange: method and get the best of both worlds. (The model and controller would usually be separate objects in a real application.)
Peter Hosey
+2  A: 
Jon Shea