'How to create TextField that only accepts numbers
I'm new to SwiftUI and iOS, and I'm trying to create an input field that will only accept numbers.
TextField("Total number of people", text: $numOfPeople)
The TextField
currently allows alphabetic characters, how do I make it so that the user can only input numbers?
Solution 1:[1]
tl;dr
Checkout John M's solution for a much better way.
One way to do it is that you can set the type of keyboard on the TextField
which will limit what people can type on.
TextField("Total number of people", text: $numOfPeople)
.keyboardType(.numberPad)
Apple's documentation can be found here, and you can see a list of all supported keyboard types here.
However, this method is only a first step and is not ideal as the only solution:
- iPad doesn't have a numberPad so this method won't work on an iPad.
- If the user is using a hardware keyboard then this method won't work.
- It does not check what the user has entered. A user could copy/paste a non-numeric value into the TextField.
You should sanitise the data that is entered and make sure that it is purely numeric.
For a solution that does that checkout John M's solution below. He does a great job explaining how to sanitise the data and how it works.
Solution 2:[2]
Although showing a number pad is a good first step, it does not actually prevent bad data from being entered:
- The user can paste the non-numeric text into the
TextField
- iPad users will still get a full keyboard
- Anyone with a Bluetooth keyboard attached can type anything
What you really want to do is sanitize the input, like this:
import SwiftUI
import Combine
struct StackOverflowTests: View {
@State private var numOfPeople = "0"
var body: some View {
TextField("Total number of people", text: $numOfPeople)
.keyboardType(.numberPad)
.onReceive(Just(numOfPeople)) { newValue in
let filtered = newValue.filter { "0123456789".contains($0) }
if filtered != newValue {
self.numOfPeople = filtered
}
}
}
}
Whenever numOfPeople
changes, the non-numeric values are filtered out, and the filtered value is compared to see if numOfPeople
should be updated a second time, overwriting the bad input with the filtered input.
Note that the Just
publisher requires that you import Combine
.
EDIT:
To explain the Just
publisher, consider the following conceptual outline of what occurs when you change the value in the TextField
:
- Because
TextField
takes aBinding
to aString
when the contents of the field are changed, it also writes that change back to the@State
variable. - When a variable marked
@State
changes, SwiftUI recomputes thebody
property of the view. - During the
body
computation, aJust
publisher is created. Combine has a lot of different publishers to emit values over time, but theJust
publisher takes "just" a single value (the new value ofnumberOfPeople
) and emits it when asked. - The
onReceive
method makes aView
a subscriber to a publisher, in this case, theJust
publisher we just created. Once subscribed, it immediately asks for any available values from the publisher, of which there is only one, the new value ofnumberOfPeople
. - When the
onReceive
subscriber receives a value, it executes the specified closure. Our closure can end in one of two ways. If the text is already numeric only, then it does nothing. If the filtered text is different, it is written to the@State
variable, which begins the loop again, but this time the closure will execute without modifying any properties.
Check out Using Combine for more info.
Solution 3:[3]
A lot easyer in my opinion is to use a custom Binding and convert any Strings into numeric values straight ahead. This way you also have the State variable as a number instead of a string, which is a huge plus IMO.
The following is all code needed. Note, that a default value is used in case a string cannot be converted (zero in this case).
@State private var myValue: Int
// ...
TextField("number", text: Binding(
get: { String(myValue) },
set: { myValue = Int($0) ?? 0 }
))
Solution 4:[4]
The ViewModifier
version of @John M.'s answer.
import Combine
import SwiftUI
public struct NumberOnlyViewModifier: ViewModifier {
@Binding var text: String
public init(text: Binding<String>) {
self._text = text
}
public func body(content: Content) -> some View {
content
.keyboardType(.numberPad)
.onReceive(Just(text)) { newValue in
let filtered = newValue.filter { "0123456789".contains($0) }
if filtered != newValue {
self.text = filtered
}
}
}
}
Solution 5:[5]
Heavily inspired by John M.'s answer, I modified things slightly.
For me, on Xcode 12 and iOS 14, I noticed that typing letters did show in the TextField
, despite me not wanting them to. I wanted letters to be ignored, and only numerals to be permitted.
Here's what I did:
@State private var goalValue = ""
var body: some View {
TextField("12345", text: self.$goalValue)
.keyboardType(.numberPad)
.onReceive(Just(self.goalValue), perform: self.numericValidator)
}
func numericValidator(newValue: String) {
if newValue.range(of: "^\\d+$", options: .regularExpression) != nil {
self.goalValue = newValue
} else if !self.goalValue.isEmpty {
self.goalValue = String(newValue.prefix(self.goalValue.count - 1))
}
}
The key here is the else if
; this sets the value of the underlying variable to be everything-but-the-most-recent-character.
Also worth noting, if you'd like to permit decimal numbers and not limit to just integers, you could change the regex string to "^[\d]+\.?[\d]+$"
, which you'll have to escape to be "^[\\d]+\\.?[\\d]+$"
.
Solution 6:[6]
It is possible to hand a NumberFormatter to the TextField and have it handle the conversion for you:
struct MyView: View {
@State private var value = 42 // Note, integer value
var body: some View {
// NumberFormatter will parse the text and cast to integer
TextField("title", value: $value, formatter: NumberFormatter())
}
}
Note that the formatter will be applied once the user finishes editing. If the user inputted a text that can't be formatted by the NumberFormatter, the value will not be changed. So this may or may not cover your question "a textfield that only accepts numbers".
Solution 7:[7]
Most of the answers have some significant drawbacks. Philip's answer is the best so far IMHO. Most of the other answers don't filter out the non-numeric characters as they are typed. Instead you have to wait until after the user finishes editing, then they update the text to remove the non-numeric characters. Then the next common issue is that they don't handle numbers when the input language doesn't use ASCII 0-9 characters for the numbers.
I came up with a solution similar to Philip's but that is more production ready. NumericText SPM Package
First you need a way to properly filter non-numeric characters from a string, that works properly with unicode.
public extension String {
func numericValue(allowDecimalSeparator: Bool) -> String {
var hasFoundDecimal = false
return self.filter {
if $0.isWholeNumber {
return true
} else if allowDecimalSeparator && String($0) == (Locale.current.decimalSeparator ?? ".") {
defer { hasFoundDecimal = true }
return !hasFoundDecimal
}
return false
}
}
}
Then wrap the text field in a new view. I wish I could do this all as a modifier. While I could filter the string in one, you loose the ability for the text field to bind a number value.
public struct NumericTextField: View {
@Binding private var number: NSNumber?
@State private var string: String
private let isDecimalAllowed: Bool
private let formatter: NumberFormatter = NumberFormatter()
private let title: LocalizedStringKey
private let onEditingChanged: (Bool) -> Void
private let onCommit: () -> Void
public init(_ titleKey: LocalizedStringKey, number: Binding<NSNumber?>, isDecimalAllowed: Bool, onEditingChanged: @escaping (Bool) -> Void = { _ in }, onCommit: @escaping () -> Void = {}) {
formatter.numberStyle = .decimal
_number = number
if let number = number.wrappedValue, let string = formatter.string(from: number) {
_string = State(initialValue: string)
} else {
_string = State(initialValue: "")
}
self.isDecimalAllowed = isDecimalAllowed
title = titleKey
self.onEditingChanged = onEditingChanged
self.onCommit = onCommit
}
public var body: some View {
return TextField(title, text: $string, onEditingChanged: onEditingChanged, onCommit: onCommit)
.onChange(of: string, perform: numberChanged(newValue:))
.modifier(KeyboardModifier(isDecimalAllowed: isDecimalAllowed))
}
private func numberChanged(newValue: String) {
let numeric = newValue.numericValue(allowDecimalSeparator: isDecimalAllowed)
if newValue != numeric {
string = numeric
}
number = formatter.number(from: string)
}
}
You don't strictly need this modifier, but it seems like you'd pretty much always want it.
private struct KeyboardModifier: ViewModifier {
let isDecimalAllowed: Bool
func body(content: Content) -> some View {
#if os(iOS)
return content
.keyboardType(isDecimalAllowed ? .decimalPad : .numberPad)
#else
return content
#endif
}
}
Solution 8:[8]
Another approach perhaps is to create a View that wraps the TextField view, and holds two values: a private var holding the entered String, and a bindable value that holds the Double equivalent. Each time the user types a character it try's to update the Double.
Here's a basic implementation:
struct NumberEntryField : View {
@State private var enteredValue : String = ""
@Binding var value : Double
var body: some View {
return TextField("", text: $enteredValue)
.onReceive(Just(enteredValue)) { typedValue in
if let newValue = Double(typedValue) {
self.value = newValue
}
}.onAppear(perform:{self.enteredValue = "\(self.value)"})
}
}
You could use it like this:
struct MyView : View {
@State var doubleValue : Double = 1.56
var body: some View {
return HStack {
Text("Numeric field:")
NumberEntryField(value: self.$doubleValue)
}
}
}
This is a bare-bones example - you might want to add functionality to show a warning for poor input, and perhaps bounds checks etc...
Solution 9:[9]
You can also use a simple formatter:
struct AView: View {
@State var numberValue:Float
var body: some View {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
return TextField("number", value: $numberValue, formatter: NumberFormatter())
}
Users can still try to enter some text as seen here:
But the formatter enforces a number to be used.
Solution 10:[10]
First post here, so please forgive any mistakes. I've been struggling with this question in my current project. Many of the answers work well, but only for particular problems, and in my case, none met all the requirements.
Specifically, I needed:
- Numeric-only user input, including negative numbers, in multiple
Text
fields. - Binding of that input to a var of type Double from an ObservableObject class, for use in multiple calculations.
John M's solution is great, but it binds to an @State private var that is a string.
jamone's answer and his NumericText solution are fantastic in many ways, and I implemented them in the iOS14 version of my project. Unfortunately, it doesn't allow for the input of negative numbers.
The solution I came up with was based mainly on John M's answer but incorporates use of onEditingChanged that I learned from jamone's NumericText code. This allows me to clean the user input text based on John M's solution, but then (with the closure called by onEditingChanged) bind that string to an Observable Object Double.
So there is really nothing new in what I have below, and it might be obvious to more experienced developers. But in all my searching I never stumbled across this solution, so I post it here in case it helps others.
import Foundation
import Combine
class YourData: ObservableObject {
@Published var number = 0
}
func convertString(string: String) -> Double {
guard let doubleString = Double(string) else { return 0 }
return doubleString
}
struct ContentView: View {
@State private var input = ""
@EnvironmentObject var data: YourData
var body: some View {
TextField("Enter string", text: $input, onEditingChanged: {
_ in self.data.number = convertString(string: self.input) })
.keyboardType(.numbersAndPunctuation)
.onReceive(Just(input)) { cleanNum in
let filtered = cleanNum.filter {"0123456789.-".contains($0)}
if filtered != cleanNum {
self.input = filtered
}
}
}
}
Solution 11:[11]
You don't need to use Combine
and onReceive
, you can also use this code:
class Model: ObservableObject {
@Published var text : String = ""
}
struct ContentView: View {
@EnvironmentObject var model: Model
var body: some View {
TextField("enter a number ...", text: Binding(get: { self.model.text },
set: { self.model.text = $0.filter { "0123456789".contains($0) } }))
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(Model())
}
}
Unfortunately there is also a small flickering, so you also can see the non-allowed characters for a very short time (in my eyes a little bit shorter as the way with Combine
)
Solution 12:[12]
The ViewModifier of @cliss answer taking into account the decimal separator for the language set on the device. Feel free to extend this solution:
// TextField+Validator.swift
import SwiftUI
import Combine
struct TextFieldValidator: ViewModifier {
enum ValidatorType: String {
case decimal = "^[-]?[\\d]*(?:\\###decimalSeparator###?[\\d]*)?$"
case number = "^\\d+$"
}
@Binding var goalValue: String
var validatorType: ValidatorType
private func validator(newValue: String) {
let regex: String = validatorType.rawValue.replacingOccurrences(of: "###decimalSeparator###", with: Locale.current.decimalSeparator!)
if newValue.range(of: regex, options: .regularExpression) != nil {
self.goalValue = newValue
} else if !self.goalValue.isEmpty {
self.goalValue = String(newValue.prefix(self.goalValue.count - 1))
}
}
func body(content: Content) -> some View {
content
.onReceive(Just(goalValue), perform: validator)
}
}
extension TextField {
func validator(goalValue: Binding<String>, type: TextFieldValidator.ValidatorType) -> some View {
modifier(TextFieldValidator(goalValue: goalValue, validatorType: type))
}
}
Number Example:
@State private var goalValue = "0"
TextField("1", text: $goalValue)
.validator(goalValue: $goalValue, type: .number)
.keyboardType(.numberPad)
Decimal Example:
@State private var goalValue = "0,0"
TextField("1.0", text: $goalValue)
.validator(goalValue: $goalValue, type: .decimal)
.keyboardType(.decimalPad)
Solution 13:[13]
I propose a version based on @John M. and @hstdt that deal with:
start with bound value
negative number
decimal separator (if more than one, cut the string)
struct NumberField : View { @Binding var value : Double @State private var enteredValue = "#START#" var body: some View { return TextField("", text: $enteredValue) .onReceive(Just(enteredValue)) { typedValue in var typedValue_ = typedValue == "#START#" ? String(self.value) : typedValue if typedValue != "" { let negative = typedValue_.hasPrefix("-") ? "-" : "" typedValue_ = typedValue_.filter { "0123456789.".contains($0) } let parts = typedValue_.split(separator: ".") let formatedValue = parts.count == 1 ? negative + String(parts[0]) : negative + String(parts[0]) + "." + String(parts[1]) self.enteredValue = formatedValue } let newValue = Double(self.enteredValue) ?? 0.0 self.value = newValue } .onAppear(perform:{ self.enteredValue = "\(self.value)" }) } }
Solution 14:[14]
Jamone who took Philip Pegden's approach to a more robust NumericTextField did us a great service. One problem I found with the approach however occurs if the NumericTextField is used in a scrollable list and part scrolls out of view. The internal state of the string can be lost causing unexpected behavior on scrolling. I also wanted to be able to enter negative numbers and exponential parts (numbers like -1.6E-19). I make a new NumericTextField that allows for options of a decimal point, an exponent and a minus sign that only contains the string. I also made a reformat function that is fired from the onEditingChanged false condition. My version works pretty well but still could use some more testing and improvements. Since a partially entered number creates updates immediately the partial entries often aren't numbers and return nil from the number converter. It seems it would be straightforward to remove the last character of the string on a failed conversion and try again until a number is returned or no more characters are left in which case a nil is returned. In general this would be the last good number entered.
If a lot of calculation occurs on a change it may be better to wait until done editing before binding, but then this is not the right textfield for that, as was the point originally at the top of the post. In any case, here is the code for my version as it is so far.
//String+Numeric.swift
import Foundation
public extension String {
/// Get the numeric only value from the string
/// - Parameter allowDecimalSeparator: If `true` then a single decimal separator will be allowed in the string's mantissa.
/// - Parameter allowMinusSign: If `true` then a single minus sign will be allowed at the beginning of the string.
/// - Parameter allowExponent: If `true` then a single e or E separator will be allowed in the string to start the exponent which can be a positive or negative integer
/// - Returns: Only numeric characters and optionally a single decimal character and optional an E followed by numeric characters.
/// If non-numeric values were interspersed `1a2b` then the result will be `12`.
/// The numeric characters returned may not be valid numbers so conversions will generally be optional strings.
func numericValue(allowDecimalSeparator: Bool = true, allowNegatives: Bool = true, allowExponent: Bool = true) -> String {
// Change parameters to single enum ?
var hasFoundDecimal = false
var allowMinusSign = allowNegatives // - can only be first char or first char after E (or e)
var hasFoundExponent = !allowExponent
var allowFindingExponent = false // initially false to avoid E as first character and then to prevent finding 2nd E
return self.filter {
if allowMinusSign && "-".contains($0){
return true
} else {
allowMinusSign = false
if $0.isWholeNumber {
allowFindingExponent = true
return true
} else if allowDecimalSeparator && String($0) == (Locale.current.decimalSeparator ?? ".") {
defer { hasFoundDecimal = true }
return !hasFoundDecimal
} else if allowExponent && !hasFoundExponent && allowFindingExponent && "eE".contains($0) {
allowMinusSign = true
hasFoundDecimal = true
allowFindingExponent = false
hasFoundExponent = true
return true
}
}
return false
}
}
This extension allows strings with minus signs and one E or e but only in the correct places.
Then the NumericTextModifier a la Jamone is
//NumericTextModifier.swift
import SwiftUI
/// A modifier that observes any changes to a string, and updates that string to remove any non-numeric characters.
/// It also will convert that string to a `NSNumber` for easy use.
public struct NumericTextModifier: ViewModifier {
/// Should the user be allowed to enter a decimal number, or an integer
public let isDecimalAllowed: Bool
public let isExponentAllowed: Bool
public let isMinusAllowed: Bool
/// The string that the text field is bound to
/// A number that will be updated when the `text` is updated.
@Binding public var number: String
/// - Parameters:
/// - number:: The string 'number" that this should observe and filter
/// - isDecimalAllowed: Should the user be allowed to enter a decimal number, or an integer
/// - isExponentAllowed: Should the E (or e) be allowed in number for exponent entry
/// - isMinusAllowed: Should negatives be allowed with minus sign (-) at start of number
public init( number: Binding<String>, isDecimalAllowed: Bool, isExponentAllowed: Bool, isMinusAllowed: Bool) {
_number = number
self.isDecimalAllowed = isDecimalAllowed
self.isExponentAllowed = isExponentAllowed
self.isMinusAllowed = isMinusAllowed
}
public func body(content: Content) -> some View {
content
.onChange(of: number) { newValue in
let numeric = newValue.numericValue(allowDecimalSeparator: isDecimalAllowed, allowNegatives: isMinusAllowed, allowExponent: isExponentAllowed).uppercased()
if newValue != numeric {
number = numeric
}
}
}
}
public extension View {
/// A modifier that observes any changes to a string, and updates that string to remove any non-numeric characters.
func numericText(number: Binding<String>, isDecimalAllowed: Bool, isMinusAllowed: Bool, isExponentAllowed: Bool) -> some View {
modifier(NumericTextModifier( number: number, isDecimalAllowed: isDecimalAllowed, isExponentAllowed: isExponentAllowed, isMinusAllowed: isMinusAllowed))
}
}
The NumericTextField then becomes:
// NumericTextField.swift
import SwiftUI
/// A `TextField` replacement that limits user input to numbers.
public struct NumericTextField: View {
/// This is what consumers of the text field will access
@Binding private var numericText: String
private let isDecimalAllowed: Bool
private let isExponentAllowed: Bool
private let isMinusAllowed: Bool
private let title: LocalizedStringKey
//private let formatter: NumberFormatter
private let onEditingChanged: (Bool) -> Void
private let onCommit: () -> Void
/// Creates a text field with a text label generated from a localized title string.
///
/// - Parameters:
/// - titleKey: The key for the localized title of the text field,
/// describing its purpose.
/// - numericText: The number to be displayed and edited.
/// - isDecimalAllowed: Should the user be allowed to enter a decimal number, or an integer
/// - isExponentAllowed:Should the user be allowed to enter a e or E exponent character
/// - isMinusAllowed:Should user be allow to enter negative numbers
/// - formatter: NumberFormatter to use on getting focus or losing focus used by on EditingChanged
/// - onEditingChanged: An action thats called when the user begins editing `text` and after the user finishes editing `text`.
/// The closure receives a Boolean indicating whether the text field is currently being edited.
/// - onCommit: An action to perform when the user performs an action (for example, when the user hits the return key) while the text field has focus.
public init(_ titleKey: LocalizedStringKey, numericText: Binding<String>, isDecimalAllowed: Bool = true,
isExponentAllowed: Bool = true,
isMinusAllowed: Bool = true,
onEditingChanged: @escaping (Bool) -> Void = { _ in },
onCommit: @escaping () -> Void = {}) {
_numericText = numericText
self.isDecimalAllowed = isDecimalAllowed || isExponentAllowed
self.isExponentAllowed = isExponentAllowed
self.isMinusAllowed = isMinusAllowed
title = titleKey
self.onEditingChanged = onEditingChanged
self.onCommit = onCommit
}
public var body: some View {
TextField(title, text: $numericText,
onEditingChanged: { exited in
if !exited {
numericText = reformat(numericText)
}
onEditingChanged(exited)},
onCommit: {
numericText = reformat(numericText)
onCommit() })
.onAppear { numericText = reformat(numericText) }
.numericText( number: $numericText, isDecimalAllowed: isDecimalAllowed, isMinusAllowed: isMinusAllowed, isExponentAllowed: isExponentAllowed )
//.modifier(KeyboardModifier(isDecimalAllowed: isDecimalAllowed))
}
}
func reformat(_ stringValue: String) -> String {
if let value = NumberFormatter().number(from: stringValue) {
let compare = value.compare(NSNumber(0.0))
if compare == .orderedSame {
return "0"
}
if (compare == .orderedAscending) { // value negative
let compare = value.compare(NSNumber(-1e-3))
if compare != .orderedDescending {
let compare = value.compare(NSNumber(-1e5))
if compare == .orderedDescending {
return value.stringValue
}
}
}
else {
let compare = value.compare(NSNumber(1e5))
if compare == .orderedAscending {
let compare = value.compare(NSNumber(1e-3))
if compare != .orderedAscending {
return value.stringValue
}
}
}
return value.scientificStyle
}
return stringValue
}
private struct KeyboardModifier: ViewModifier {
let isDecimalAllowed: Bool
func body(content: Content) -> some View {
#if os(iOS)
return content
.keyboardType(isDecimalAllowed ? .decimalPad : .numberPad)
#else
return content
#endif
}
}
I used the func reformat(String) -> String rather than a formatter directly. Reformat uses a couple of formatters and was more flexible at least to me.
import Foundation
var decimalNumberFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.allowsFloats = true
return formatter
}()
var scientificFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .scientific
formatter.allowsFloats = true
return formatter
}()
extension NSNumber {
var scientificStyle: String {
return scientificFormatter.string(from: self) ?? description
}
}
I hope some of this helps others who want to use scientific notation and negative numbers in their app.
Happy coding.
Solution 15:[15]
Change text: -> value: and add the format modifier.
Now you can handle everything you need to. I would just go with this:
TextField("Total Number of people:", value: $numOfPeople, format:.number)
.keyboardType(.numberPad)
This should be good for 99% of your problems. You can type Strings in there, but they will be filtered out and don't crash your app.
Solution 16:[16]
PositiveNumbersTextField Heavily inspired by what was written here (Thanks all!) I came up with a slightly different solution that fits my needs and answers the original question above using the .onChange modifier. The text field will only except input of positive numbers allowing 1 decimal point, 0, or empty. The sanitizer will remove extra decimal points, multiple zeros at start, decimal at start and any character that is not a number (except the 1 decimal). This does not support negative numbers (-).
struct PositiveNumbersTextField: View {
@Binding var textFieldText: String
var body: some View {
TextField("", text: $textFieldText)
.keyboardType(.decimalPad)
.onChange(of: textFieldText) { text in
textFieldText = text.sanitizeToValidPositiveNumberOrEmpty()
}
}
}
private extension String {
func sanitizeToValidPositiveNumberOrEmpty() -> String {
var sanitized: String
// Remove multiple decimal points except the first one if exists.
let groups = self.components(separatedBy: ".")
if groups.count > 1 {
sanitized = groups[0] + "." + groups.dropFirst().joined()
} else {
sanitized = self
}
// Remove characters that are not numbers or decimal point
sanitized = sanitized.filter { $0.isNumber || $0 == "." }
// Don't allow decimal point at start
if sanitized.first == "." {
sanitized.removeFirst()
}
// Remove any number after 0 (if first number is zero)
if sanitized.first == "0" {
var stringIndicesToRemove = [String.Index]()
for index in 1..<sanitized.count {
let stringIndex = sanitized.index(sanitized.startIndex, offsetBy: index)
if sanitized[stringIndex] == "." {
break // no need to iterate through anymore
}
stringIndicesToRemove.append(stringIndex)
}
for stringIndexToRemove in stringIndicesToRemove.reversed() {
sanitized.remove(at: stringIndexToRemove)
}
}
return sanitized
}
}
Solution 17:[17]
Here as a variant based on John M's solution, that avoids Combine, supports any value type, and allows for validating of the output value, so that it only uses the input string if it's parseable and validated.
Example usage, that keeps the bound value > 0:
@State var count: Int
…
GenericEntryField(value: $count, validate: { $0 > 0 })
struct GenericEntryField<T: Equatable>: View {
@Binding var value: T
let stringToValue: (String) -> T?
let validate: (T) -> Bool
@State private var enteredText: String = ""
var body: some View {
return TextField(
"",
text: $enteredText,
onEditingChanged: { focussed in
if !focussed {
// when the textField is defocussed, reset the text back to the bound value
enteredText = "\(self.value)"
}
}
)
.onChange(of: enteredText) { newText in
// whenever the text-field changes, try to convert it to a value, and validate it.
// if so, use it (this will update the enteredText)
if let newValue = stringToValue(newText),
validate(newValue) {
self.value = newValue
}
}
.onChange(of: value) { newValue in
// whenever value changes externally, update the string
enteredText = "\(newValue)"
}
.onAppear(perform: {
// update the string based on value at start
enteredText = "\(value)"
})
}
}
extension GenericEntryField {
init(value: Binding<Int>, validate: @escaping (Int) -> Bool = { _ in true }) where T == Int {
self.init(value: value, stringToValue: { Int($0) }, validate: validate)
}
init(value: Binding<Double>, validate: @escaping (Double) -> Bool = { _ in true }) where T == Double {
self.init(value: value, stringToValue: { Double($0) }, validate: validate)
}
}
Solution 18:[18]
TextField that accepts only numbers:
textField("", text: Binding(
get: {inputNum},
set: {inputNum = $0.filter{"0123456789".contains($0)}}))
.textFieldStyle(RoundedBorderTextFieldStyle())
Convert numeric input to Int:
let n: Int = NumberFormatter().number(from: "0" + inputNum) as! Int
Solution 19:[19]
Expanding John M.
's example to accept only one period .
or one comma ,
for international decimals.
Thanks John M.
struct TextFieldCharacterRestrictions: View {
@State private var numOfPeople = ""
var body: some View {
TextField("Total number of people", text: $numOfPeople)
.keyboardType(.decimalPad)
.onChange(of: numOfPeople){newValue in
let periodCount = newValue.components(separatedBy: ".").count - 1
let commaCount = newValue.components(separatedBy: ",").count - 1
if newValue.last == "." && periodCount > 1 || newValue.last == "," && commaCount > 1{
//it's a second period or comma, remove it
numOfPeople = String(newValue.dropLast())
// as bonus for the user, add haptic effect
let generator = UINotificationFeedbackGenerator()
generator.prepare()
generator.notificationOccurred(.warning)
}else{
let filtered = newValue.filter { "0123456789.,".contains($0) }
if filtered != newValue{
self.numOfPeople = filtered
}
}
}
}
}
Solution 20:[20]
I made an extension based on John M's answer, all you have to do is add the following code to your project:
import SwiftUI
import Combine
struct TextFieldSanitize: ViewModifier {
@Binding private var text: String
private let allowedChars: String
init(text: Binding<String>, allowedChars: String) {
self.allowedChars = allowedChars
self._text = text
}
func body(content: Content) -> some View {
content
.onReceive(Just(text)) { newValue in
let filtered = newValue.filter { Set(allowedChars).contains($0) }
if filtered != newValue { text = filtered }
}
}
}
extension View {
func onlyAcceptingAllowedChars(_ allowedChars: String, in text: Binding<String>) -> some View {
modifier(TextFieldSanitize(text: text, allowedChars: allowedChars))
}
func onlyAcceptingDouble(in text: Binding<String>) -> some View {
let decimalSeparator = Locale.current.decimalSeparator ?? "."
let allowedChars = "0123456789\(decimalSeparator)"
return onlyAcceptingAllowedChars(allowedChars, in: text)
}
func onlyAcceptingInt(in text: Binding<String>) -> some View {
let allowedChars = "0123456789"
return onlyAcceptingAllowedChars(allowedChars, in: text)
}
}
Usage:
If you want to create a TextField that only accepts integers, you can follow the example below:
import SwiftUI
struct StackOverflowTests: View {
@State private var numOfPeople = "0"
var body: some View {
TextField("Total number of people", text: $numOfPeople)
.keyboardType(.numberPad)
.onlyAcceptingInt(in: $numOfPeople)
}
}
The same can be done for Double
by using the onlyAcceptingDouble
method instead.
If you want to make a custom sanitizer, like a TextField that only accepts "A", "2" and "%" as characters for example, just call the onlyAcceptingAllowedChars
method like this:
import SwiftUI
struct StackOverflowTests: View {
@State private var customText = ""
var body: some View {
TextField("Custom text", text: $customText)
.onlyAcceptingAllowedChars("A2%", in: $customText)
}
}
This answer was tested in a project with iOS 14 as target.
Solution 21:[21]
This solution worked great for me. It will auto format it as a number once committed, and you can add your own custom validation if desired - in my case I wanted a maximum value of 100.
@State private var opacity = 100
TextField("Opacity", value: $opacity, format: .number)
.onChange(of: opacity) { newValue in
if newValue > 100 {
opacity = 100
}
}
.keyboardType(.numberPad)
.multilineTextAlignment(.center)
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow