How to create a swiftui textfield that allows the user to only input numbers and a single dot? In other words, it checks digit by digit as the user inputs, if the input is a number or a dot and the textfield doesn't have another dot the digit is accepted, otherwise the digit entry is ignored. Using a stepper isn't an option.
You just make a View with all the code you want for the SecureTextField and the TextField then all you have to do is call the HybridTextField where ever you need it. Save this answer.
You can create a custom text field and add a value to make it become first responder. Note: didBecomeFirstResponder is needed to make sure the text field becomes first responder only once, not on every refresh by SwiftUI !
SwiftUI doesn't let you specify a set of allowed characters for a TextField
. Actually, it's not something related to the UI itself, but to how you manage the model behind. In this case the model is the text behind the TextField
. So, you need to change your view model.
If you use the $
sign on a @Published
property you can get access to the Publisher
behind the @Published
property itself. Then you can attach your own subscriber to the publisher and perform any check you want. In this case I used the sink
function to attach a closure based subscriber to the publisher:
/// Attaches a subscriber with closure-based behavior.
///
/// This method creates the subscriber and immediately requests an unlimited number of values, prior to returning the subscriber.
/// - parameter receiveValue: The closure to execute on receipt of a value.
/// - Returns: A cancellable instance; used when you end assignment of the received value. Deallocation of the result will tear down the subscription stream.
public func sink(receiveValue: @escaping ((Self.Output) -> Void)) -> AnyCancellable
The implementation:
import SwiftUI
import Combine
class ViewModel: ObservableObject {
@Published var text = ""
private var subCancellable: AnyCancellable!
private var validCharSet = CharacterSet(charactersIn: "1234567890.")
init() {
subCancellable = $text.sink { val in
//check if the new string contains any invalid characters
if val.rangeOfCharacter(from: self.validCharSet.inverted) != nil {
//clean the string (do this on the main thread to avoid overlapping with the current ContentView update cycle)
DispatchQueue.main.async {
self.text = String(self.text.unicodeScalars.filter {
self.validCharSet.contains($0)
})
}
}
}
}
deinit {
subCancellable.cancel()
}
}
struct ContentView: View {
@ObservedObject var viewModel = ViewModel()
var body: some View {
TextField("Type something...", text: $viewModel.text)
}
}
Important to note that:
$text
($
sign on a @Published
property) gives us an object of type Published<String>.Publisher
i.e. a publisher$viewModel.text
($
sign on an @ObservableObject
) gives us an object of type Binding<String>
That are two completely different things.
EDIT: If you want you can even create you own custom TextField
with this behaviour. Let's say you want to create a DecimalTextField
view:
import SwiftUI
import Combine
struct DecimalTextField: View {
private class DecimalTextFieldViewModel: ObservableObject {
@Published var text = ""
private var subCancellable: AnyCancellable!
private var validCharSet = CharacterSet(charactersIn: "1234567890.")
init() {
subCancellable = $text.sink { val in
//check if the new string contains any invalid characters
if val.rangeOfCharacter(from: self.validCharSet.inverted) != nil {
//clean the string (do this on the main thread to avoid overlapping with the current ContentView update cycle)
DispatchQueue.main.async {
self.text = String(self.text.unicodeScalars.filter {
self.validCharSet.contains($0)
})
}
}
}
}
deinit {
subCancellable.cancel()
}
}
@ObservedObject private var viewModel = DecimalTextFieldViewModel()
var body: some View {
TextField("Type something...", text: $viewModel.text)
}
}
struct ContentView: View {
var body: some View {
DecimalTextField()
}
}
This way you can use your custom text field just writing:
DecimalTextField()
and you can use it wherever you want.
This is a simple solution for TextField validation: (updated)
struct ContentView: View {
@State private var text = ""
func validate() -> Binding<String> {
let acceptableNumbers: String = "0987654321."
return Binding<String>(
get: {
return self.text
}) {
if CharacterSet(charactersIn: acceptableNumbers).isSuperset(of: CharacterSet(charactersIn: $0)) {
print("Valid String")
self.text = $0
} else {
print("Invalid String")
self.text = $0
self.text = ""
}
}
}
var body: some View {
VStack {
Spacer()
TextField("Text", text: validate())
.padding(24)
Spacer()
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With