如何创建仅接受数字和单个点的 SwiftUI TextField?
How to create SwiftUI TextField that accepts only numbers and a single dot?
如何创建一个只允许用户输入数字和一个点的 swiftui 文本框?
换句话说,它会在用户输入时逐位检查,如果输入是数字或点并且文本字段没有其他点,则接受数字,否则忽略数字输入。
使用步进器不是一种选择。
SwiftUI 不允许您为 TextField
指定一组允许的字符。实际上,这与 UI 本身无关,而是与您如何管理背后的模型有关。在这种情况下,模型是 TextField
后面的文本。所以,你需要改变你的视图模型。
如果您在 @Published
属性 上使用 $
标志,您可以访问 @Published
属性 后面的 Publisher
本身。然后您可以将自己的订阅者附加到发布者并执行您想要的任何检查。在这种情况下,我使用 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([=11=])
})
}
}
}
}
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([=12=])
})
}
}
}
}
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()
而且你可以随时随地使用它。
这是 TextField 验证的简单解决方案:(更新)
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: [=10=])) {
print("Valid String")
self.text = [=10=]
} else {
print("Invalid String")
self.text = [=10=]
self.text = ""
}
}
}
var body: some View {
VStack {
Spacer()
TextField("Text", text: validate())
.padding(24)
Spacer()
}
}
}
我认为使用异步调度是错误的方法,可能会导致其他问题。这是一个使用 Double
支持的 属性 实现相同功能的实现,并在您每次在绑定视图中键入时手动迭代字符。
final class ObservableNumber: ObservableObject {
let precision: Int
@Published
var value: String {
didSet {
var decimalHit = false
var remainingPrecision = precision
let filtered = value.reduce(into: "") { result, character in
// If the character is a number that by adding wouldn't exceed the precision and precision is set then add the character.
if character.isNumber, remainingPrecision > 0 || precision <= 0 {
result.append(character)
// If a decimal has been hit then decrement the remaining precision to fulfill
if decimalHit {
remainingPrecision -= 1
}
// If the character is a decimal, one hasn't been added already, and precision greater than zero then add the decimal.
} else if character == ".", !result.contains("."), precision > 0 {
result.append(character)
decimalHit = true
}
}
// Only update value if after processing it is a different value.
// It will hit an infinite loop without this check since the published event occurs as a `willSet`.
if value != filtered {
value = filtered
}
}
}
var doubleValue: AnyPublisher<Double, Never> {
return $value
.map { Double([=10=]) ?? 0 }
.eraseToAnyPublisher()
}
init(precision: Int, value: Double) {
self.precision = precision
self.value = String(format: "%.\(precision)f", value)
}
}
此解决方案还确保您只有一个小数点,而不是允许 "."
.
的多个实例
注意额外计算的 属性 将其“放回”到 Double
。这使您可以继续将数字作为数字而不是 String
做出反应,并且必须在任何地方都 cast/convert。您可以很容易地添加任意数量的计算属性作为 Int
或任何数字类型,只要您以您期望的方式转换它。
再注意你也可以把它变成通用的ObservableNumber<N: Numeric>
并处理不同的输入,但是使用Double
并把泛型排除在外会简化其他路上的事情。根据您的需要更改。
如何创建一个只允许用户输入数字和一个点的 swiftui 文本框? 换句话说,它会在用户输入时逐位检查,如果输入是数字或点并且文本字段没有其他点,则接受数字,否则忽略数字输入。 使用步进器不是一种选择。
SwiftUI 不允许您为 TextField
指定一组允许的字符。实际上,这与 UI 本身无关,而是与您如何管理背后的模型有关。在这种情况下,模型是 TextField
后面的文本。所以,你需要改变你的视图模型。
如果您在 @Published
属性 上使用 $
标志,您可以访问 @Published
属性 后面的 Publisher
本身。然后您可以将自己的订阅者附加到发布者并执行您想要的任何检查。在这种情况下,我使用 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([=11=])
})
}
}
}
}
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([=12=])
})
}
}
}
}
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()
而且你可以随时随地使用它。
这是 TextField 验证的简单解决方案:(更新)
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: [=10=])) {
print("Valid String")
self.text = [=10=]
} else {
print("Invalid String")
self.text = [=10=]
self.text = ""
}
}
}
var body: some View {
VStack {
Spacer()
TextField("Text", text: validate())
.padding(24)
Spacer()
}
}
}
我认为使用异步调度是错误的方法,可能会导致其他问题。这是一个使用 Double
支持的 属性 实现相同功能的实现,并在您每次在绑定视图中键入时手动迭代字符。
final class ObservableNumber: ObservableObject {
let precision: Int
@Published
var value: String {
didSet {
var decimalHit = false
var remainingPrecision = precision
let filtered = value.reduce(into: "") { result, character in
// If the character is a number that by adding wouldn't exceed the precision and precision is set then add the character.
if character.isNumber, remainingPrecision > 0 || precision <= 0 {
result.append(character)
// If a decimal has been hit then decrement the remaining precision to fulfill
if decimalHit {
remainingPrecision -= 1
}
// If the character is a decimal, one hasn't been added already, and precision greater than zero then add the decimal.
} else if character == ".", !result.contains("."), precision > 0 {
result.append(character)
decimalHit = true
}
}
// Only update value if after processing it is a different value.
// It will hit an infinite loop without this check since the published event occurs as a `willSet`.
if value != filtered {
value = filtered
}
}
}
var doubleValue: AnyPublisher<Double, Never> {
return $value
.map { Double([=10=]) ?? 0 }
.eraseToAnyPublisher()
}
init(precision: Int, value: Double) {
self.precision = precision
self.value = String(format: "%.\(precision)f", value)
}
}
此解决方案还确保您只有一个小数点,而不是允许 "."
.
注意额外计算的 属性 将其“放回”到 Double
。这使您可以继续将数字作为数字而不是 String
做出反应,并且必须在任何地方都 cast/convert。您可以很容易地添加任意数量的计算属性作为 Int
或任何数字类型,只要您以您期望的方式转换它。
再注意你也可以把它变成通用的ObservableNumber<N: Numeric>
并处理不同的输入,但是使用Double
并把泛型排除在外会简化其他路上的事情。根据您的需要更改。