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

前端 未结 2 1531
無奈伤痛
無奈伤痛 2021-01-06 04:02

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

相关标签:
2条回答
  • 2021-01-06 05:02

    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()
        }
      }
    }
    
    0 讨论(0)
  • 2021-01-06 05:04

    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.

    0 讨论(0)
提交回复
热议问题