Skip to content

Latest commit

 

History

History
51 lines (43 loc) · 2.84 KB

README.md

File metadata and controls

51 lines (43 loc) · 2.84 KB

NumberField: Real-time NumberFormatter Validation of TextField Input

One of the most frustrating parts of building SwiftUI-based apps at the moment is dealing with input validation of numeric values. As of this writing, the native support for numberFormatters in TextFields appears to be broken.

Various folks[1] have suggested that the way around this is to create a Binding<String> that manages conversion between string and numeric values. This is indeed possible -- and is part of what this code does. But there is currently an additional problem with TextField, and IIRC, it's also a maddening problem with UITextField in UIKit: Input validation (and the associated conversion of string to numeric values) happens either too often or not often enough.

In a naive version of the use-a-binding approach, string to numeric and back to string conversion occurs with each keystroke, which is a nightmare if you are using a currency format. The string "1" gets converted to 1.0 which gets converted to "$1.00" with the insertion point not between the dollar sign and the numeral one. Comma and decimal point handling is similarly frustrating for the user.

If you try to deal with this problem by implementing string validation and conversion only when focus is lost, you are now in a position where your numeric state object does not reflect the most recent value entered by the user. This makes it impossible to type "12.34" and then tap a button that then consumes the converted decimal. More subtley, you will not be able to guard the button's enabled state using something like Button("Add"){ ... }.disabled(myNumber==nil).

One way of partially mitigating some of these issues is using onCommit: but its behavaior if not broken is at least awkward. Most importantly, your onCommit: closure (only) fires when the user taps return on a text field, which means 1) more stale value problems and 2) using the decimal pad keyboard means your onCommit: handler will never be fired.

I finally figured out an approach that works well for my needs, and it is based on the insight that things get much simpler if an additional numeric value is introduced. One Decimal holds the most recent numeric value as of the last loss of focus, which reflects the formatted appearance of the field when the user is not interacting with it. The other Decimal holds the most recent converted value as of the last keystroke. The insight is that you want to convert on a per-keystroke basis but you do not want to use that conversion to update the text field string contents until after the user is no longer typing in the field.

Please enjoy. Pull requests et c. welcome.

[1]: See this Stack Overflow post and this Twitter thread.