Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create SwiftUI TextField that accepts only numbers and a single dot?

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.

like image 414
M.Serag Avatar asked Sep 06 '19 13:09

M.Serag


People also ask

How do I create a secure TextField in SwiftUI?

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.

How do you make a TextField first responder in SwiftUI?

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 !


2 Answers

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.

like image 194
matteopuc Avatar answered Nov 15 '22 06:11

matteopuc


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()
    }
  }
}
like image 37
FRIDDAY Avatar answered Nov 15 '22 06:11

FRIDDAY