Numbers only TextField with SwiftUI

Numbers only TextField with SwiftUI

Requiring an input to be numbers only is quite a common task. In this tutorial I will show you how to allow numbers only in a TextField using SwiftUI.

Step 1: Change the keyboard type to .decimalPad

The first step in this task is to change the keyboard type to .decimalPad. This is the code that I am using for this tutorial:

struct ContentView: View {
    @State var input = ""
    
    var body: some View {
        TextField("Input", text: $input)
            .padding()
            .keyboardType(.decimalPad)
    }
}

As you can see, I have a textfield and and I have used the .keyboardType view modifier to show the .decimalPad which will only allow numbers as input.

There is a big issue with doing this, it does not prevent your user from pasting text into this input as you can see below:

Ok so now that we see the problem, how do we fix this? It is quite an easy fix, but in some ways it also feels a bit overkill.

Step 2: ObservableObject to force numbers only

We need to use Combine to force numbers as the only input. To do this we are going to create a new class called NumbersOnly which will adopt ObservableObject. It will then Publish the value once the value has been cleaned.

Add the following class to your project:

class NumbersOnly: ObservableObject {
    @Published var value = "" {
        didSet {
            let filtered = value.filter { $0.isNumber }
            
            if value != filtered {
                value = filtered
            }
        }
    }
}

So what do we have here? We have a simple value property that we will be observed. This value property uses the didSet property observer to apply the logic that will prevent any non numeric values being inserted into the text field.

What is the logic? it is really simple, we use filter on value, in the filter we check each character to see if it is a number, if it is not a number it will return false and filter will remove that character. Filter will return a new string that will have no non numbers in it.

Now that we have the filtered string, we check to see if value is equal to filtered, if it is not the same then we will assign filtered to value otherwise we do nothing.

Before we can see if this works, we need to update our ContentView to look like this:

struct ContentView: View {
    @ObservedObject var input = NumbersOnly()
    
    var body: some View {
        TextField("Input", text: $input.value)
            .padding()
            .keyboardType(.decimalPad)
    }
}

All that we have done here is to update input. It is now using @ObservedObject instead of @State. We also initialise NumbersOnly and assign it to input. Lastly we update our TextField to use $input.value instead of $input.

If we build and run the app now you will see that we cannot paste non numeric values: (NOTE: I added in a print statement so that you can see in the console that I am trying to paste text)

Conclusion

It is a bit unfortunate that we have to use combine for something this simple, but I think that is just how SwiftUI is going to be. In some ways I quite like it since it really puts emphasis on separating logic and ui and it also tries to make us break things down into their smallest parts.

It has been brought to my attention that the below solution doesn't work well on iOS 14.1. I tested this and it seems like iOS 14.0.1 and iOS 14.1 have this issue, iOS 14.2 and iOS 14.4 seem to work as expected.

If you want the full source code, you can find it here.