Validating string in TextField with NumberFormatter in SwiftUI
I was looking into creating a view which has TextField with NumberFormatter. Typed text would show up in a separate label and when trying to enter non-numbers, the TextField would reject those characters. Although TextField component in SwiftUI has generic initialiser init(_:value:formatter:onEditingChanged:onCommit:) it does not seem to do what we need. Value binding does not update while typing, non-number characters are not discarded, and string is not reloaded when view reloads with different model data. Therefore, I decided to create a wrapper around TextField which deals with transforming numbers to strings and implements all the before mentioned features.
Content view with temperature limits
Example use-case is basic view for editing temperature limits where model type will force high value to be at least 10 units higher compared to low value. The model type also have separate properties for getting NSNumber instances what we use later (such conversion could also happen on SwiftUI level).
Content view has text fields, button for randomising limits and label for displaying current values. NumberTextField is a custom view which implements all the features what we listed in the beginning of the post.
Creating NumberTextField with NSNumber binding and NumberFormatter
NumberTextField is a wrapper around TextField and internally handles NSNumber to String and String to NSNumber transformations. Transformations happen inside a separate class called StringTransformer which stores editable string in @Published property. @Published property is first populated with string value by transforming NSNumber to String using the formatter. Changes made by user are captured by subscribing to stringValue publisher (@Published properties provide publishers). String to NSNumber transformation is tried when user edits the string: if successful, NSNumber is send back to model using the value binding, if fails, stringValue is set back to previous value. Note that dropFirst skips initial update when setting up sink and receive operator is used for scheduling updates at later time when SwiftUI has finished current layout update cycle.
TextField’s formatter initialiser does not seem to be operating as expected and therefore we built a custom view. It handles number to string transformations and refreshes the view when string can’t be transformed to number. Hopefully future SwiftUI iterations will fix the init(_:value:formatter:onEditingChanged:onCommit:) initialiser and NumberTextField is not needed at all.
SwiftUIFormattedTextField (Xcode 11.4.1)