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

落爺英雄遲暮 提交于 2020-01-01 18:59:27

问题


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.


回答1:


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.



来源:https://stackoverflow.com/questions/57822749/how-to-create-swiftui-textfield-that-accepts-only-numbers-and-a-single-dot

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!