'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:

  1. iPad doesn't have a numberPad so this method won't work on an iPad.
  2. If the user is using a hardware keyboard then this method won't work.
  3. 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:

  1. The user can paste the non-numeric text into the TextField
  2. iPad users will still get a full keyboard
  3. 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:

  1. Because TextField takes a Binding to a String when the contents of the field are changed, it also writes that change back to the @State variable.
  2. When a variable marked @State changes, SwiftUI recomputes the body property of the view.
  3. During the body computation, a Just publisher is created. Combine has a lot of different publishers to emit values over time, but the Just publisher takes "just" a single value (the new value of numberOfPeople) and emits it when asked.
  4. The onReceive method makes a View a subscriber to a publisher, in this case, the Just 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 of numberOfPeople.
  5. 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:

demo

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:

  1. Numeric-only user input, including negative numbers, in multiple Text fields.
  2. 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)