如何创建一个swiftui文本字段,允许用户仅输入数字和单个点?换句话说,它在用户输入时逐位检查,如果输入是数字或点,并且文本字段没有其他点,则接受该数字,否则忽略该数字条目。不能使用步进器。
SwiftUI不允许您为指定一组允许的字符TextField
。实际上,这与UI本身无关,而与您如何管理背后的模型有关。在这种情况下,模型是后面的文字TextField
。因此,您需要更改视图模型。
如果$
在@Published
属性上使用符号,则可以访问属性本身Publisher
后面的符号@Published
。然后,您可以将自己的订阅者附加到发布者,并执行所需的任何检查。在这种情况下,我使用该sink
函数将基于闭包的订阅者附加到发布者:
/// 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
实现:
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)
}
}
重要说明:
$text
($
在@Published
属性上签名)为我们提供了一个类型的对象,Published<String>.Publisher
即发布者$viewModel.text
($
在上签名@ObservableObject
)给我们一个类型的对象Binding<String>
那是两件事完全不同。
编辑:如果您愿意,甚至可以TextField
使用此行为创建自己的自定义。假设您要创建一个DecimalTextField
视图:
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()
}
}
这样,您可以使用自定义文本字段编写:
DecimalTextField()
您可以在任何地方使用它。
如何在现有应用中使用?即DecimalTextField($ myValue)?
@caram我做了一个可行的例子。它不是完美的,可以改进,但是没有更好。在那里我这样使用它: DecimalTextField("123", numericValue: $numeric)
参考代码如下:
import SwiftUI
import Combine
fileprivate func getTextOn(double: Double) -> String{
let rounded = double - Double(Int(double)) == 0
var result = ""
if double != Double.zero{
result = rounded ? String(Int(double)) : String(double)
}
return result
}
struct DecimalTextField: View {
public let placeHolder: String
@Binding var numericValue: Double
private class DecimalTextFieldViewModel: ObservableObject {
@Published var text = ""{
didSet{
DispatchQueue.main.async {
let substring = self.text.split(separator: Character("."), maxSplits: 2)
if substring.count == 0{
if self.numericValue != 0{
self.numericValue = 0
}
}else if substring.count == 1{
var newValue: Double = 0
if let lastChar = substring[0].last{
let ch = String(lastChar)
if ch == "."{
newValue = Double(String(substring[0]).dropLast()) ?? 0
}else{
newValue = Double(String(substring[0])) ?? 0
}
}
if self.numericValue != newValue{
self.numericValue = newValue
}
}else{
let newValue = Double(String("\(String(substring[0])).\(String(substring[1]))")) ?? 0
if self.numericValue != newValue{
self.numericValue = newValue
}
}
}
}
}
private var subCancellable: AnyCancellable!
private var validCharSet = CharacterSet(charactersIn: "1234567890.")
@Binding private var numericValue: Double{
didSet{
DispatchQueue.main.async {
if String(self.numericValue) != self.text {
self.text = String(self.numericValue)
}
}
}
}
init(numericValue: Binding<Double>, text: String) {
self.text = text
self._numericValue = numericValue
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
init(_ placeHolder: String = "", numericValue: Binding<Double>){
self._numericValue = numericValue
self.placeHolder = placeHolder
self.viewModel = DecimalTextFieldViewModel(numericValue: self._numericValue, text: getTextOn(double: numericValue.wrappedValue))
}
var body: some View {
TextField(placeHolder, text: $viewModel.text)
.keyboardType(.decimalPad)
}
}
struct testView: View{
@State var numeric: Double = 0
var body: some View{
return VStack(alignment: .center){
Text("input: \(String(numeric))")
DecimalTextField("123", numericValue: $numeric)
}
}
}
struct decimalTextField_Previews: PreviewProvider {
static var previews: some View {
testView()
}
}
非常感谢您提供了这个非常有用的答案,它教会了我很多关于Combine和SwiftUI使用pub / sub的知识。我相信可以简化此方法,以消除显式订阅以及由此引起的线程问题。请参见本要点,以获取包括使用示例的工作示例。
本文翻译自stackoverflow.com,查看原文请点击:ios - How to create SwiftUI TextField that accepts only numbers and a single dot?