How to create TextField that only accepts numbers
Asked Answered
S

28

77

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?

Shawna answered 6/11, 2019 at 15:1 Comment(3)
Note that TextField has an init methid that takes a Formatter as argument – Selfimportant
@JoakimDanielson can you perhaps assist in showing how I can use the formattter? – Shawna
I have a solution which involves creating a simple IntField, which takes about a dozen lines of code. See #56799956 – Alt
W
60

There are several different initialisers that you can use for TextField that take a formatter/format and allow you to use a binding that is not a String. This allows you to ensure that only number values can be entered.

For iOS 13+ we can use the following initialiser:

init<S, V>(S, value: Binding<V>, formatter: Formatter)

struct TextFieldExample: View {
    @State private var number: Double? // Make this optional to allow an empty TextField
    var body: some View {
        TextField("Placeholder", value: $number, formatter: NumberFormatter())
    }
}

Or for iOS 15+ you can use the following

init<S, F>(S, value: Binding<F.FormatInput>, format: F, prompt: Text?)

struct TextFieldExample: View {
    @State private var number: Double?
    var body: some View {
        TextField("Placeholder", value: $number, format: .number)
    }
}

In both of these, entering anything other than a number will cause the TextField to reject the input.

Both of these methods will present a standard keyboard, if you want to make the UX better you could set the keyboardType to be .numberPad or .decimalPad. Just remember that if you use these keyboard types then you will need to add a button to the keyboard to dismiss you can do this using the .toolbar modifier and @FocusState, you'll also have to handle what happens when the user presses your toolbar button as onSubmit doesn't seem to be called when the FocusState is released.

If you want to have an empty TextField, when the user hasn't entered anything then make sure that your Double value is optional. If you would rather it always has a value, make sure that your Double value is not optional.


Previous answer

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.

Westcott answered 6/11, 2019 at 15:34 Comment(8)
This is exactly what I was looking for πŸ‘ – Shawna
Im experiencing a minor issue, the keyboard wont go away when Im done typing, Any idea why? – Shawna
@LupyanaMbembati this SO question/answer has several suggestions on how to hide the keyboard once you have finished with it. #56491886 – Westcott
This does not actually prevent non-numeric input; see my answer. – Venturous
John is right, this doesn't prevent from entering non-numeric input from a hardware keyboard. – Bots
Tks for documentation links and explanation about hardware keyboard. – Hyrcania
But what if they paste a text without typing? will this solution work? – Visitant
@Visitant if you use the TextField initialisers that take a value and a formatter or a format then it will allow it to be pasted but the TextField won't accept it when you press return. – Westcott
V
188

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.

Venturous answered 6/11, 2019 at 18:2 Comment(22)
I just started looking into Swift / SwiftUI (coming from Windows C# and Web Typescript world), and the first issue I ran into was how to filter user input to allow only numbers. It is very common to filter input based on a regex expression. Your solution seem to be exactly what I was looking for- so big thank you. I looked at the documentation and it seems a bit lacking at best. Would you mind explaining the reason for the 'Just' publisher – Radiocarbon
+1 for this answer. However, I do not quiet understand importing Combine for that. What is Just doing. Thanks – Bacillary
@JesperKristiansen @Bacillary I added an explanation of the Just publisher. – Venturous
Thank you for that nice explanation! Your solution works, but for a short moment you can see the old value before it gets filtered. Is there a way to prevent this? – Turnaround
And by the way: .onReceive is called everytime, when sth. else in the View get's changed. Isn't this a bit too heavy? – Turnaround
This solution leads to cycling problem because it changes value of observed property. The good solution is to use the "formatter:" param of TextField? – Jair
Excellent conceptual outline, compliments – Rattly
How would this change if the value was an actual Int instead of a String that happened to have a number in it? I mean, if the OP is trying to restrict the input to only numbers, then presumably somewhere else in the code, it's going to be used/saved as an Int, not a String. – Genaro
@Genaro The @State variable has to be a String, because TextField requires a Binding<String>. So wherever the number is saved, you will have to handle the optional that results from Int(numOfPeople). – Venturous
@JohnM. Ok, thanks. That's what I feared. I went for a slider in my own app. – Genaro
what about using a NumberFormatter? – Disharoon
Excellent solution, works like charm, thanks! TIP: For improve performance wrap referenced numbers to Set like that Set("0123456789").contains($0). With that small upgrade, contains function now works with constant O(1), instead of linear O(n) complexity. It's small, but useful improvements. – Clad
@MoritzSchaub - the issue with a NumberFormatter based solution is that although it will prevent the bound value from being set incorrectly, it still allows TextField to display the wrong thing, i.e. TextField's keeping it's own cached copy that goes out of sync. Been driving me nuts for the last week - Xcode 13 beta broke my MVVM code that had this working using a custom Binding(get:set:) filter approach. More on that over here developer.apple.com/forums/thread/684433 .Fwiw this Just publisher based approach is the best workaround for TextField's behaviour I've found. – Burnsed
Excellent answer and explanation. – Rew
I wish more answers on Stack were like this. Often times a solution is posted, with very little "why" behind it and or explanation. Great job. Thanks – Deena
Great answer, though is there a way to get the oldValue? – Lysis
Does not work on simulator (iOS 15.4). I wonder whether this solution works on the real device. – Microgram
@MichaelPohl That might not actually be faster. Yes, the theoretical worst-case time complexity is lower, but searching over an array of 10 characters might be faster than the hash function used by Set. Searching the 10 chars might take an average of 30 CPU cycles while the hash function takes an average of 50 CPU cycles. You'd have to measure in this case to get a good estimate. – Doublebreasted
Small correct to my last comment: the worst-case time complexity of a hashtable, like Set uses, is actually still O(n) because of collision potential. The average/amortized complexity of a hashtable is, however, O(1). – Doublebreasted
@Doublebreasted Looks like you are right. I tried this filter in Google Benchmark and plain string is much faster than Set, even if the contains function has O(1) complexity. Tested on the same string of course. So sorry everyone for my two-year-old comment. :-/ Test: Plain String 1792.000 ns Test: Set String 13750.000 ns – Clad
@MichaelPohl I'm impressed that you actually tried it! Yeah, no sweat. I just wanted to point that out because we, as programmers, tend to get caught up too much in theoretical complexity without thinking about the actual costs. If an array has a known size, we might actually consider searching that array a constant time operation (where the constant in this case is the cost of searching 10 items) rather than a linear time operation. Obviously in the case where the array could have anywhere from 10 to 1,000,000 items, the O(n) complexity would play a bigger role. – Doublebreasted
We also forget that the worst case of a hash table, or the big-oh, is actually O(n) (a table where everything collides is just a slow list). The general, practical case, however, is O(1). Source: en.wikipedia.org/wiki/Hash_table – Doublebreasted
W
60

There are several different initialisers that you can use for TextField that take a formatter/format and allow you to use a binding that is not a String. This allows you to ensure that only number values can be entered.

For iOS 13+ we can use the following initialiser:

init<S, V>(S, value: Binding<V>, formatter: Formatter)

struct TextFieldExample: View {
    @State private var number: Double? // Make this optional to allow an empty TextField
    var body: some View {
        TextField("Placeholder", value: $number, formatter: NumberFormatter())
    }
}

Or for iOS 15+ you can use the following

init<S, F>(S, value: Binding<F.FormatInput>, format: F, prompt: Text?)

struct TextFieldExample: View {
    @State private var number: Double?
    var body: some View {
        TextField("Placeholder", value: $number, format: .number)
    }
}

In both of these, entering anything other than a number will cause the TextField to reject the input.

Both of these methods will present a standard keyboard, if you want to make the UX better you could set the keyboardType to be .numberPad or .decimalPad. Just remember that if you use these keyboard types then you will need to add a button to the keyboard to dismiss you can do this using the .toolbar modifier and @FocusState, you'll also have to handle what happens when the user presses your toolbar button as onSubmit doesn't seem to be called when the FocusState is released.

If you want to have an empty TextField, when the user hasn't entered anything then make sure that your Double value is optional. If you would rather it always has a value, make sure that your Double value is not optional.


Previous answer

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.

Westcott answered 6/11, 2019 at 15:34 Comment(8)
This is exactly what I was looking for πŸ‘ – Shawna
Im experiencing a minor issue, the keyboard wont go away when Im done typing, Any idea why? – Shawna
@LupyanaMbembati this SO question/answer has several suggestions on how to hide the keyboard once you have finished with it. #56491886 – Westcott
This does not actually prevent non-numeric input; see my answer. – Venturous
John is right, this doesn't prevent from entering non-numeric input from a hardware keyboard. – Bots
Tks for documentation links and explanation about hardware keyboard. – Hyrcania
But what if they paste a text without typing? will this solution work? – Visitant
@Visitant if you use the TextField initialisers that take a value and a formatter or a format then it will allow it to be pasted but the TextField won't accept it when you press return. – Westcott
D
44

A lot easier 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 }
))
Dibble answered 20/12, 2020 at 22:55 Comment(3)
This is really nice and succinct. Thanks. – Travis
Yeah works cool for Int but unfortunately does not do the trick for floats – Nerty
That is correct. The string representation of a floating point value, e.g. String(myFloatValue), is in the format \d+\.\d+. This collides with the natural human way of writing floating point numbers as it enforces the floating part at any time. – Dibble
T
24

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())
            .keyboardType(.numberPad)
    }
}

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".

The following example features:

  • Double value
  • Custom number formatter
  • Make sure the value stays in a certain range
struct MyView: View {
    @State private var value = 42.5 // Double

    let formatter: NumberFormatter = {
        let formatter = NumberFormatter()
        formatter.numberStyle = .decimal
        formatter.minimumFractionDigits = 1
        formatter.maximumFractionDigits = 1
        return formatter
    }()

    var body: some View {
        // NumberFormatter will parse the text and cast to Double
        TextField("title", value: $value, formatter: formatter)
            // DecimalPad for floating point number input
            .keyboardType(.decimalPad)
            // Make sure value stays between 0 and 30 (inclusive)
            .onChange(of: value) { value = min(30, max(0, value)) }
    }
}
Tudela answered 15/1, 2021 at 15:29 Comment(0)
H
18

Update: Binding<String> is a better solution

link: https://mcmap.net/q/203967/-how-to-detect-live-changes-on-textfield-in-swiftui

====

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
                }
            }
    }
}

Hawkweed answered 24/5, 2020 at 10:35 Comment(0)
A
13

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]+$".

Ahead answered 24/9, 2020 at 14:4 Comment(8)
I'd like to offer this regex to allow negative numbers, and to allow decimals, but not require two digits: "^[-]?[\\d]*(?:\\.?[\\d]*)?$" – Parallelogram
Is there anyway to add a variable to numeric validator? This solution only has a single textfield, but I have mulitple, so I'll need to pass in a variable to change the textfield var accordingly. Just adding another variable to numericValidator doesn't work – Hexamerous
Casey has five answers on SO, and I stumble across one of them while listening to ATP. Funny! BTW: It helped, thanks! – Amid
@Amid ha! SAME HERE. What a small world it is 😜. I bet this was something Casey came across while working on GoalTender! – Schnorr
@Schnorr Correct; this came out of trying to limit things in Goaltender. πŸ˜‡ – Ahead
I need to allow decimals but for some reason, the suggested solution "^[\\d]+\\.?[\\d]+$" by cliss doesn't work in Xcode 14 iPhone 14 Pro simulator. @StephenB. the regex you proposed works fine except that I would like to allow , commas for regions where the period is not used, how could I allow commas in your regex "^[\\d]*(?:\\.?[\\d]*)?$"? Sorry but I feel blind with regex, I haven't been able to find good tutorials. – Betake
Here is the final regex expression that prevents entering a comma or a period if one already exists. "^\\d+[.,]?\\d*$|^[.,]\\d*$". Thanks – Betake
Actually, this answer doesn't satisfy all the criteria; the else if clause causes a problem if the user tries to enter non-numeric characters in the middle of the string. – Yasmeen
B
9

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
    }
}
Barnie answered 12/7, 2020 at 23:13 Comment(0)
S
7

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...

Seesaw answered 15/4, 2020 at 18:42 Comment(0)
A
7

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.

Alvis answered 15/7, 2021 at 14:5 Comment(0)
T
5

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)

Turnaround answered 14/4, 2020 at 12:51 Comment(0)
H
5

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
                }
            }
        }
}
Hereby answered 27/7, 2020 at 17:58 Comment(0)
N
2

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)
Norenenorfleet answered 5/12, 2021 at 20:47 Comment(0)
B
2

After playing around with some of the code provided in some of the answers from this thread I came up with two solutions, one that uses Combine and another one without it.

Without Using Combine.

The simplest way to limit to only numbers without using Combine would be as follow.

struct TextFieldNumbersOnly: View {

    @State private var textFieldOne = ""
    
    var body: some View {
        Form {
            TextField("field one", text: $textFieldOne)
                //.keyboardType(.numberPad)// uncomment for production
                .onChange(of: textFieldOne){ newValue in
                    textFieldOne = allowNumbers(newValue)
                }
        }
    }

    func allowNumbers(_ inputValue: String) -> String {
        let filtered = inputValue.filter { "0123456789".contains($0) }
        return filtered
    }
}

Now, if you want to allow decimals, a comma , or a period . add the following method.

func allowDecimalAndNumbers(_ inputValue: String) -> String {
    let periodCount = inputValue.components(separatedBy: ".").count - 1
    let commaCount = inputValue.components(separatedBy: ",").count - 1
    
    if inputValue.last == "." && periodCount > 1 || inputValue.last == "," && commaCount > 1 {
        //it's a second period or comma, remove it
        return String(inputValue.dropLast())
    } else {
        let filtered = inputValue.filter { "0123456789.,".contains($0) }
        if filtered != inputValue{
            return filtered
        }
    }
    return inputValue
}

Using Combine

The following code allows only numbers and a period or a comma.

import Combine
struct TextFieldNumbersOnly: View {
    
    @State private var inputField1 = ""

    var body: some View {
        TextField("enter numbers", text: self.$inputField1)
            //.keyboardType(.decimalPad) // uncomment for production
            .onReceive(Just(self.inputField1)) { value in
               inputField1 = numericValidator(inputValue: value)
            }
    }

    func numericValidator(inputValue: String)->String {
        if inputValue.range(of: #"^\d+[.,]?\d*$|^[.,]\d*$"#, options: .regularExpression) != nil {
            return  inputValue
        } else if !self.inputField1.isEmpty {
            return String(inputValue.prefix(self.inputField1.count - 1))
        }
        return ""
    }
}
Betake answered 23/2, 2022 at 15:13 Comment(1)
Will this work for localisation? – Scroggins
J
1

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)"
          })
      }
    }
    
Jair answered 15/8, 2020 at 22:18 Comment(0)
A
1

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.

Arvid answered 22/2, 2021 at 22:1 Comment(0)
A
1

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.

Alcina answered 6/1, 2022 at 2:50 Comment(0)
B
1

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.

Burgonet answered 11/4, 2022 at 3:13 Comment(0)
T
1

There's a lot of solutions on this thread already, but none of which were reusable, generic and covering the case of an empty textfield:

extension TextField where Label == Text {
    init<V: LosslessStringConvertible>(_ titleKey: LocalizedStringKey, value: Binding<V?>) {
        self.init(titleKey, text: Binding<String>(
            get: {
                if let value = value.wrappedValue{
                    return String(value)
                } else {
                    return String()
                }
            }, set: { text in
                if text.isEmpty {
                    value.wrappedValue = nil
                } else if let v = V(text) {
                    value.wrappedValue = v
                }
            }
        ))
    }
}

Usage:

struct ContentView: View {
    @State var value: Int? = 0
    
    var body: some View {
        VStack {
            TextField("Test", value: $value)
            
            Text("The value is \(value ?? -1)")
        }
    }
}
Titanium answered 15/8, 2022 at 16:34 Comment(0)
F
1

You can trim the characters that are not numbers (using .decimalDigits.inverted):

import SwiftUI 
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 
                    self.numOfPeople =  newValue.trimmingCharacters(in: .decimalDigits.inverted)
                
            }
    }
}
Falla answered 13/10, 2022 at 19:48 Comment(0)
P
1

Swift UI complete solution

  1. TextField allow numeric value only
  2. Should accept only one comma (".")
  3. Restrict decimal point upto x decimal place

File NumbersOnlyViewModifier

import Foundation
import SwiftUI
import Combine
struct NumbersOnlyViewModifier: ViewModifier {
    
    @Binding var text: String
    var includeDecimal: Bool
    var digitAllowedAfterDecimal: Int = 1
    
    func body(content: Content) -> some View {
        content
            .keyboardType(includeDecimal ? .decimalPad : .numberPad)
            .onReceive(Just(text)) { newValue in
                var numbers = "0123456789"
                let decimalSeparator: String = Locale.current.decimalSeparator ?? "."
                if includeDecimal {
                    numbers += decimalSeparator
                }
                if newValue.components(separatedBy: decimalSeparator).count-1 > 1 {
                    let filtered = newValue
                    self.text = isValid(newValue: String(filtered.dropLast()), decimalSeparator: decimalSeparator)
                } else {
                    let filtered = newValue.filter { numbers.contains($0)}
                    if filtered != newValue {
                        self.text = isValid(newValue: filtered, decimalSeparator: decimalSeparator)
                    } else {
                        self.text = isValid(newValue: newValue, decimalSeparator: decimalSeparator)
                    }
                }
            }
    }
    
    private func isValid(newValue: String, decimalSeparator: String) -> String {
        guard includeDecimal, !text.isEmpty else { return newValue }
        let component = newValue.components(separatedBy: decimalSeparator)
        if component.count > 1 {
            guard let last = component.last else { return newValue }
            if last.count > digitAllowedAfterDecimal {
                let filtered = newValue
               return String(filtered.dropLast())
            }
        }
        return newValue
    }
}

File View+Extenstion

extension View {
    func numbersOnly(_ text: Binding<String>, includeDecimal: Bool = false) -> some View {
        self.modifier(NumbersOnlyViewModifier(text: text, includeDecimal: includeDecimal))
    }
} 

File ViewFile

 TextField("", text: $value,  onEditingChanged: { isEditing in
      self.isEditing = isEditing
   })

  .foregroundColor(Color.neutralGray900)
  .numbersOnly($value, includeDecimal: true)
  .font(.system(size: Constants.FontSizes.fontSize22))
  .multilineTextAlignment(.center)
Pimple answered 27/12, 2022 at 21:46 Comment(0)
J
1

Improved Answer

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 { $0.isNumber }
            if filtered != newValue {
                self.numOfPeople = filtered
            }
        }
}

}

Jacey answered 14/2, 2023 at 5:47 Comment(0)
X
0

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
}
}
Xylotomy answered 3/11, 2021 at 7:55 Comment(0)
N
0

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)
    }
}
Nor answered 18/2, 2022 at 20:5 Comment(0)
R
0

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)
Realpolitik answered 24/4, 2022 at 21:11 Comment(0)
C
0

Easiest Way TextField("Longitude", value:$longitude, formatter:NumberFormatter()) .keyboardType(.numberPad)

Concepcionconcept answered 8/9, 2022 at 9:40 Comment(2)
See how to format code on this site – Westcott
As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center. – Nephelinite
B
0

Here's what I came up with before I found this thread. It is a bit different to the others so far...

func numericTextField(_ str: String, _ bstr: Binding<String>) -> some View {
    return TextField("float", text: bstr)
        .keyboardType(.numbersAndPunctuation)
        .padding(8)
        .background(SwiftUI.Color.gray)
        .foregroundColor(Double(str) == nil ? Color.red : Color.white)
}

Here, the entry text colour goes red when the current string is not a valid float. This does not prevent you returning a string that cannot be parsed into a Double or whatever, but it does give you a visual feedback. I should use the proper alert colours.

The string I pass in is a @State variable, which is why I needed to pass it in twice to get the $string version inside the function. There may be a more elegant way to do this, but this worked for me. The string is used to update a double in the redraw code if the string is valid. This is used to update a number. A call looks like this...

numericTextField(str, $str)

I use the .numbersAndPunctuation keyboard because I need the minus sign, and occasionally need the 'e' for exponents. The keyboard is a bit larger than the decimal one, but it is the same size on the screen as the regular keyboard, so the page layout does not dance about if you switch between editing strings and numbers.

Postscript:

Having looked at the other solutions, here is my new NumberFormatter version...

func doubleTextField(_ val: Binding<Double>) -> some View {
    let fmt = NumberFormatter()
    fmt.numberStyle = .decimal
    return TextField("float", value: val, formatter: fmt)
        .keyboardType(.numbersAndPunctuation)
        .padding(8)
        .background(SwiftUI.Color.gray)

I liked my colour-changing text that let you know when you had typed something wrong, but this is nearer to what an ordinary TextField does with strings.

Barrack answered 7/10, 2022 at 19:9 Comment(0)
I
0
import Combine
import SwiftUI

struct ContentView: View {
    @State private var numOfPeople = ""

    private let numberOnlyFormatter: NumberFormatter = {
        let formatter = NumberFormatter()
        formatter.numberStyle = .decimal
        formatter.maximumFractionDigits = 0
        return formatter
    }()
    
    var body: some View {
        TextField("Number of People", text: $numOfPeople)
            .keyboardType(.numberPad)
            .textContentType(.oneTimeCode)
            .onReceive(Just(numOfPeople)) { newValue in
                let filtered = newValue.filter { "0123456789".contains($0) }
                if filtered != newValue {
                    self.numOfPeople = filtered
                }
            }
            .padding()
    }
}

This is what I use

Iaria answered 11/3, 2023 at 16:57 Comment(0)
K
0

If you are using decimal numbers and you want to check that there is only one dot before converting to double:

@State var length = ""
...
TextField("0", text: $length)
      .onChange(of: length) {
    let filtered = length.filter { $0.isNumber || $0 == "." } //Automatically delete unwanted characters
    if length != filtered {
        length = filtered
    }
    let dotsNumber = length.filter{ $0 == "." }.count
    if dotsNumber > 1 {
        var oneDot = "" //Avoids also Copy Pasting dots
        for character in length {
            if character == "." {
                if !oneDot.contains(character) {
                    oneDot.append(character)
                }
            } else { oneDot.append(character) }
        }
        length = oneDot
    }
}
Kaela answered 16/12, 2023 at 5:36 Comment(0)

© 2022 - 2024 β€” McMap. All rights reserved.