NSFormatter allows invalid values

April 25 2020 by Jeff Johnson

I've written before about a caveat with NSFormatter. Now I'm writing about one again. I've discovered what I consider to be a bug in NSFormatter and NSTextField. One of the roles of a formatter is to prevent the user from entering invalid characters into a text field. For example, if you have a text field that is designed to take a number, you might limit entry in the text field to numerical digits. This is accomplished by subclassing NSFormatter and implementing the method isPartialStringValid: proposedSelectedRange: originalString: originalSelectedRange: errorDescription: to return NO when it detects invalid characters. However, I've discovered a situation in which that method returns NO, but the text field still adds an invalid character to its string value.

When you close a window that contains text fields, you want to make sure to end editing before the window closes, otherwise unexpected and undesirable things can happen, such as losing data. This is especially important if the window is a sheet. The standard way to end editing in a window is by calling -[NSWindow makeFirstResponder:] to make the field editor resign first responder. Which usually works well… unless there's a formatter. How does the failure occur in this case? There are some characters on the Mac that require two subsequent keystrokes to produce. For example, ô is produced by pressing option-i to start the accent, followed by pressing o. If you press option-i to start the accent, but then call -[NSWindow makeFirstResponder:] without pressing another key, then the ˆ character gets added to the text field's string value, even if the character is excluded by the formatter, and the isPartialStringValid: method returns NO.

I've created a sample Xcode project to demonstrate the bug. When you run the sample app, press option-i in the text field, and then press the "makeFirstResponder" button, you can see that the ˆ character is allowed by the text field rather than disallowed, even though you can hear the NSBeep that's called by the NSFormatter method returning NO. You can also see that makeFirstResponder is successful, the text field resigns first responder, and the button becomes the new first responder. I've tested and can reproduce this bug on macOS 10.15, 10.14, and 10.13. Notice that, strangely, the controlTextDidChange: delegate method never gets called, even though the text field string value does in fact change.

The workaround I've found to the bug is to call abortEditing on the text field. This causes the partially completed accent to disappear and not get added to the string value. The sample app includes an "abortEditing" button to demonstrate the workaround.

Jeff Johnson (My apps, PayPal.Me)