NumberTextField
Author: Mike Centers
A powerful SwiftUI text field that handles formatting and retaining number values.
GitHub
Overview:
NumberTextField
is package for SwiftUI
that offers live formatting of a text field. Rather than accepting a string for binding, NumberTextField
requires an optional Decimal
binding. This allows the developer to only worry about the underlying value
of the text field.
Requirements:
macOS(v12), iOS(v15), tvOS(v15), watchOS(v8)
* Still under review. iOS building/testing at this time. *
Usage:
struct ContentView: View {
var numberFormatter: NumberFormatter {
let f = NumberFormatter()
/*
Setup your formatter.
*/
f.numberStyle = .percent
f.maximumFractionDigits = 5
return f
}
@State var value: Decimal? = 1.5
var body: some View {
NumberTextField("Enter here...", value: self.$value,
formatter: self.numberFormatter,
isActive: self.$textFieldIsActive,
onChange: { num in
// num is a Decimal? type
},
onCommit: { num in
// num is a Decimal? type
})
.inputAccessory {
HStack {
Spacer()
Button(action: { submit() }) {
Text("Submit")
}
}
.padding()
.background(Color(.secondarySystemBackground))
}
}
}
Value
The value
parameter for the NumberTextField
is a binding to an optional Decimal
type. This binding is always updated to the current text field change. The value
can also have a value assigned prior to binding to the NumberTextField
, for if the developer wants the user to update the value
.
value = nil
- This would indicate an empty string or an underlying
nil
text property to theUIOpenTextField
. - In either case, this would indicate no value is assigned to the binding.
NumberFormatter
The NumberTextField
requires a NumberFormatter
to operate properly. This parameter allows customization of how numbers are to be displayed and emitted. However the NumberFormatter
is setup, the NumberTextField
should always respect the attributes set via the developer.
Formatter Setup
var numberFormatter: NumberFormatter {
let f = NumberFormatter()
/*
It is recommended to set the following parameters for every formatter.
These can be of whatever value you choose, just make sure they are set.
*/
f.numberStyle = .decimal // Set the style first.
f.minimumFractionDigits = 3
f.maximumFractionDigits = 7
return f
}
Formatter Attributes
NumberFormatter.numberStyle
Percent
The NumberTextField
expects the value
to be in a decimal format.
- 100% would be "value = 1.0"
- 50% would be "value = 0.5"
Fractional Digits
These attributes have a default value unique to the NumberFormatter.numberStyle
attribute. If fractional input is not performing as expected, set the .maximumFractionDigits
and .minimumFractionDigits
attributes to the desired output.
NumberFormatter.alwaysShowDecimalSeparator
The .alwaysShowDecimalSeparator
attribute is manipulated via the Coordinator
. If the developer chooses to not allow fractional input, set the .maximumFractionalDigits
attribute to zero. This will also filter the decimal separator from user input.
Keyboard
When using the .keyboardType
modifier of a View
, the UIKit text field will not receive the modification. The KeyboardType
assigned to the text field is the .decimalPad
. There will be no button to call the resignFirstResponder() method. To resolve this, a keyboard accessory must be made within SwiftUI to toggle the state of the text field.
View Modifiers
struct ContentView: View {
var numberFormatter: NumberFormatter { ... }
@State var value: Decimal? = 1.5
var body: some View {
NumberTextField("Enter here...", value: self.$value,
formatter: self.numberFormatter,
isActive: self.$textFieldIsActive,
onChange: { _ in },
onCommit: { _ in })
.uiFont(.body, weight: .semibold, design: .rounded)
.textColor(self.textFieldIsActive ? .primary : .white)
.textAlignment(.center)
}
}
.uiFont
Set the font to be displayed within the text field.
The modifier will accept a UIFont
for complete control of the font. It will also accept dynamic text types, including weight and design modification.
.textColor
Set the text foreground color.
.textAlignment
Set the text alignment.
.inputAccessory
Create an input accessory view that is bound to the keyboard.
Current Issues / Objectives
In no particular order
UI
- The text field always places the cursor towards the trailing end of the text field on changes.
- The cursor will remain in bounds.
- Inserting still works.
- This does not break the text field; however, the cursor retaining the position is necessary.
Access Control
- The package needs to be scanned for access control of stuctures and classes prior to major release.
ViewRep.UpdateUI
-
Will require a catch to check for changes prior to calling the updateText() method.
- At current, UpdateText() is called twice on every change.
- The way updating UITextField.text is designed, the value is formatted and assigned to the text field.
- At current, UpdateText() is called twice on every change.
-
A resolution to this would be: When the onChange delegate event is called:
- Update the text first with proper formatting, then assign the value.
- When the updateUIView method is called, check for changes in the value and text before calling the updateText() method.
-
updateUIView is required to assign the initial text and update the text field when external changes in value occur.
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<NumberTextFieldViewRep>) {
// Insert a check for change in uiView.text
DispatchQueue.main.async {
context.coordinator.updateText(uiView)
}
}
Change Log
v0.2.2
- Resolved error with the NumberTextField not updating on certain model changes.
v0.2.1
- Added support for the creation of an input accessory view within SwiftUI.
- Call .inputAccessory and build the view.
v0.2.0
- Fixed bug found with cursor placement.
(Current Build: Will Update tag to reflect the upcoming changes.)
- Moved View modifiers to the environment.
- NumberTextField_TextAlignment
- NumberTextField_TextColor
- NumberTextField_Font
v0.1.5
-
Added support for View modifiers:
- Multiline Text Alignment
- Font
- Only system dynamic fonts(.title, .headline, .body, etc.) work at this time.
- To display a custom font, set the font within the ViewRepresentable.
-
Resolved view sizing
-
Added the KeyboardType.decimalPad as the keyboard.
-
Added a binding to control the state of the text field.
v0.1.4
- Resolved the issue with cursor not moving correctly within bounds when a whitespace is added via the
NumberFormatter
. The cursor will now only stay within the range of the first and last number character. - All instances of the
NumberFormatter.Locale
should be supported. - All fractional digit settings are supported.
- Currency values, and those with a
.minimumFractionDigits
greater than 0, will retain the correct formatting while the text field is not active. While active, the text field allows the end-user to freely stay within the.maximumFractionDigits
.
v0.1.3
-
Added percent format support.
-
NumberFormatter
now assigns thevalue
andtext
properties. -
Enhanced tracking of text field changes to refactor unnecessary code.
-
Extended moveCursorWithinBounds() to support percent numbers.
Found Bug:
- Update text is being called on update of @State value due to the updateUI method of ViewRepresentable.
- While this is supposed to happen, updateUI method should not update the text if the values are the same.
v0.1.2
-
Added currency format support.
-
Fixed issue where input is not allowing trailing zeroes within fractional numbers.
v0.1.1
- Added decimal format support.
v0.1.0
- The initial release.
- Currently supports percent format for US or similar decimal formats.