我是 SwiftUI 和 iOS 的新手,我正在尝试创建一个只接受数字的输入字段。
TextField("Total number of people", text: $numOfPeople)
目前TextField
允许使用字母字符,我该如何使用户只能输入数字?
我是 SwiftUI 和 iOS 的新手,我正在尝试创建一个只接受数字的输入字段。
TextField("Total number of people", text: $numOfPeople)
目前TextField
允许使用字母字符,我该如何使用户只能输入数字?
虽然显示数字键盘是一个很好的第一步,但它实际上并不能防止输入错误数据:
您真正想要做的是清理输入,如下所示:
import SwiftUI
import Combine
struct StackOverflowTests: View {
@State private var numOfPeople = "0"
var body: some View {
TextField("Total number of people", text: $numOfPeople)
.keyboardType(.numberPad)
.onReceive(Just(numOfPeople)) { newValue in
let filtered = newValue.filter { "0123456789".contains($0) }
if filtered != newValue {
self.numOfPeople = filtered
}
}
}
}
每当numOfPeople
发生变化时,非数字值都会被过滤掉,并比较过滤后的值以查看是否numOfPeople
应该再次更新,用过滤后的输入覆盖错误的输入。
请注意,Just
发布者要求您import Combine
.
编辑:
为了解释Just
发布者,请考虑以下关于更改 中的值时发生的情况的概念大纲TextField
:
TextField
需要 aBinding
到 a String
,所以当字段的内容发生更改时,它也会将该更改写回@State
变量。@State
发生变化时,SwiftUI 会重新计算body
视图的属性。body
计算过程中,Just
会创建一个发布者。Combine 有很多不同的发布者随着时间的推移发出值,但是Just
发布者“只”接受一个值( 的新值numberOfPeople
)并在被询问时发出它。onReceive
方法使View
订阅者成为发布者,在本例中,是Just
我们刚刚创建的发布者。订阅后,它会立即向发布者请求任何可用值,其中只有一个,即 的新值numberOfPeople
。onReceive
订阅者收到一个值时,它会执行指定的闭包。我们的关闭可以以两种方式之一结束。如果文本已经只是数字,那么它什么也不做。如果过滤后的文本不同,则将其写入@State
变量,该变量再次开始循环,但这一次闭包将在不修改任何属性的情况下执行。查看使用组合了解更多信息。
一种方法是您可以设置键盘类型,TextField
这将限制人们可以输入的内容。
TextField("Total number of people", text: $numOfPeople)
.keyboardType(.numberPad)
可以在此处找到 Apple 的文档,您可以在此处查看所有支持的键盘类型的列表。
然而,这种方法只是第一步,作为唯一的解决方案并不理想:
您应该清理输入的数据并确保它是纯数字的。
在我看来,更容易的是使用自定义绑定并将任何字符串直接转换为数值。这样,您还可以将 State 变量作为数字而不是string,这是 IMO 的一大优势。
以下是所需的所有代码。请注意,在无法转换字符串的情况下使用默认值(在这种情况下为零)。
@State private var myValue: Int
// ...
TextField("number", text: Binding(
get: { String(myValue) },
set: { myValue = Int($0) ?? 0 }
))
ViewModifier
@John M. 的答案版本。
import Combine
import SwiftUI
public struct NumberOnlyViewModifier: ViewModifier {
@Binding var text: String
public init(text: Binding<String>) {
self._text = text
}
public func body(content: Content) -> some View {
content
.keyboardType(.numberPad)
.onReceive(Just(text)) { newValue in
let filtered = newValue.filter { "0123456789".contains($0) }
if filtered != newValue {
self.text = filtered
}
}
}
}
对我来说,在 Xcode 12 和 iOS 14 上,我注意到输入字母确实显示在 中TextField
,尽管我不希望它们出现。我希望忽略字母,只允许使用数字。
这是我所做的:
@State private var goalValue = ""
var body: some View {
TextField("12345", text: self.$goalValue)
.keyboardType(.numberPad)
.onReceive(Just(self.goalValue), perform: self.numericValidator)
}
func numericValidator(newValue: String) {
if newValue.range(of: "^\\d+$", options: .regularExpression) != nil {
self.goalValue = newValue
} else if !self.goalValue.isEmpty {
self.goalValue = String(newValue.prefix(self.goalValue.count - 1))
}
}
这里的关键是else if
; 这将基础变量的值设置为除了最近的字符之外的所有内容。
另外值得注意的是,如果您想允许十进制数字而不是仅限于整数,您可以将正则表达式字符串更改为"^[\d]+\.?[\d]+$"
,您必须将其转义为"^[\\d]+\\.?[\\d]+$"
.
可以将 NumberFormatter 交给 TextField 并让它为您处理转换:
struct MyView: View {
@State private var value = 42 // Note, integer value
var body: some View {
// NumberFormatter will parse the text and cast to integer
TextField("title", value: $value, formatter: NumberFormatter())
}
}
请注意,格式化程序将在用户完成编辑后应用。如果用户输入了 NumberFormatter 无法格式化的文本,则该值不会改变。因此,这可能会或可能不会涵盖您的问题“仅接受数字的文本字段”。
大多数答案都有一些明显的缺点。菲利普的回答是迄今为止最好的恕我直言。大多数其他答案在键入时不会过滤掉非数字字符。相反,您必须等到用户完成编辑后,然后他们更新文本以删除非数字字符。然后下一个常见问题是当输入语言不使用 ASCII 0-9 字符作为数字时,它们不处理数字。
我想出了一个类似于 Philip 的解决方案,但更适合生产。数字文本 SPM 包
首先,您需要一种从字符串中正确过滤非数字字符的方法,该方法适用于 unicode。
public extension String {
func numericValue(allowDecimalSeparator: Bool) -> String {
var hasFoundDecimal = false
return self.filter {
if $0.isWholeNumber {
return true
} else if allowDecimalSeparator && String($0) == (Locale.current.decimalSeparator ?? ".") {
defer { hasFoundDecimal = true }
return !hasFoundDecimal
}
return false
}
}
}
然后将文本字段包装在新视图中。我希望我能把这一切作为修饰符。虽然我可以将字符串过滤为一个,但您失去了文本字段绑定数字值的能力。
public struct NumericTextField: View {
@Binding private var number: NSNumber?
@State private var string: String
private let isDecimalAllowed: Bool
private let formatter: NumberFormatter = NumberFormatter()
private let title: LocalizedStringKey
private let onEditingChanged: (Bool) -> Void
private let onCommit: () -> Void
public init(_ titleKey: LocalizedStringKey, number: Binding<NSNumber?>, isDecimalAllowed: Bool, onEditingChanged: @escaping (Bool) -> Void = { _ in }, onCommit: @escaping () -> Void = {}) {
formatter.numberStyle = .decimal
_number = number
if let number = number.wrappedValue, let string = formatter.string(from: number) {
_string = State(initialValue: string)
} else {
_string = State(initialValue: "")
}
self.isDecimalAllowed = isDecimalAllowed
title = titleKey
self.onEditingChanged = onEditingChanged
self.onCommit = onCommit
}
public var body: some View {
return TextField(title, text: $string, onEditingChanged: onEditingChanged, onCommit: onCommit)
.onChange(of: string, perform: numberChanged(newValue:))
.modifier(KeyboardModifier(isDecimalAllowed: isDecimalAllowed))
}
private func numberChanged(newValue: String) {
let numeric = newValue.numericValue(allowDecimalSeparator: isDecimalAllowed)
if newValue != numeric {
string = numeric
}
number = formatter.number(from: string)
}
}
您并不严格需要此修饰符,但似乎您几乎总是需要它。
private struct KeyboardModifier: ViewModifier {
let isDecimalAllowed: Bool
func body(content: Content) -> some View {
#if os(iOS)
return content
.keyboardType(isDecimalAllowed ? .decimalPad : .numberPad)
#else
return content
#endif
}
}
另一种方法可能是创建一个包装 TextField 视图的视图,并保存两个值:一个私有 var 保存输入的字符串,一个可绑定值保存 Double 等价物。每次用户键入一个字符时,它都会尝试更新 Double。
这是一个基本的实现:
struct NumberEntryField : View {
@State private var enteredValue : String = ""
@Binding var value : Double
var body: some View {
return TextField("", text: $enteredValue)
.onReceive(Just(enteredValue)) { typedValue in
if let newValue = Double(typedValue) {
self.value = newValue
}
}.onAppear(perform:{self.enteredValue = "\(self.value)"})
}
}
你可以像这样使用它:
struct MyView : View {
@State var doubleValue : Double = 1.56
var body: some View {
return HStack {
Text("Numeric field:")
NumberEntryField(value: self.$doubleValue)
}
}
}
这是一个简单的示例 - 您可能想要添加功能以显示输入不良的警告,也许还有边界检查等......
您还可以使用简单的格式化程序:
struct AView: View {
@State var numberValue:Float
var body: some View {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
return TextField("number", value: $numberValue, formatter: NumberFormatter())
}
用户仍然可以尝试输入一些文本,如下所示:
但是格式化程序强制使用一个数字。
您不需要使用Combine
and onReceive
,您也可以使用以下代码:
class Model: ObservableObject {
@Published var text : String = ""
}
struct ContentView: View {
@EnvironmentObject var model: Model
var body: some View {
TextField("enter a number ...", text: Binding(get: { self.model.text },
set: { self.model.text = $0.filter { "0123456789".contains($0) } }))
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(Model())
}
}
不幸的是还有一个小的闪烁,所以你也可以在很短的时间内看到不允许的字符(在我眼里有点短Combine
)
第一次发帖,如有错误请见谅。在我目前的项目中,我一直在努力解决这个问题。许多答案都很好,但仅适用于特定问题,就我而言,没有一个满足所有要求。
具体来说,我需要:
John M 的解决方案很棒,但它绑定到一个字符串 @State 私有变量。
jamone 的回答,他的 NumericText 解决方案在很多方面都很棒,我在我的项目的 iOS14 版本中实现了它。不幸的是,它不允许输入负数。
我想出的解决方案主要基于 John M 的回答,但结合了我从 jamone 的 NumericText 代码中学到的 onEditingChanged 的使用。这允许我根据 John M 的解决方案清理用户输入文本,然后(使用 onEditingChanged 调用的闭包)将该字符串绑定到 Observable Object Double。
所以我在下面的内容中确实没有什么新东西,对于更有经验的开发人员来说可能是显而易见的。但是在我所有的搜索中,我从未偶然发现过这个解决方案,所以我把它贴在这里以防它帮助其他人。
import Foundation
import Combine
class YourData: ObservableObject {
@Published var number = 0
}
func convertString(string: String) -> Double {
guard let doubleString = Double(string) else { return 0 }
return doubleString
}
struct ContentView: View {
@State private var input = ""
@EnvironmentObject var data: YourData
var body: some View {
TextField("Enter string", text: $input, onEditingChanged: {
_ in self.data.number = convertString(string: self.input) })
.keyboardType(.numbersAndPunctuation)
.onReceive(Just(input)) { cleanNum in
let filtered = cleanNum.filter {"0123456789.-".contains($0)}
if filtered != cleanNum {
self.input = filtered
}
}
}
}
我提出了一个基于@John M. 和@hstdt 的版本来处理:
从绑定值开始
负数
小数点分隔符(如果超过一个,则截断字符串)
struct NumberField : View {
@Binding var value : Double
@State private var enteredValue = "#START#"
var body: some View {
return TextField("", text: $enteredValue)
.onReceive(Just(enteredValue)) { typedValue in
var typedValue_ = typedValue == "#START#" ? String(self.value) : typedValue
if typedValue != "" {
let negative = typedValue_.hasPrefix("-") ? "-" : ""
typedValue_ = typedValue_.filter { "0123456789.".contains($0) }
let parts = typedValue_.split(separator: ".")
let formatedValue = parts.count == 1 ? negative + String(parts[0]) : negative + String(parts[0]) + "." + String(parts[1])
self.enteredValue = formatedValue
}
let newValue = Double(self.enteredValue) ?? 0.0
self.value = newValue
}
.onAppear(perform:{
self.enteredValue = "\(self.value)"
})
}
}
@cliss答案的 ViewModifier考虑了设备上设置的语言的小数点分隔符。随意扩展此解决方案:
// TextField+Validator.swift
import SwiftUI
import Combine
struct TextFieldValidator: ViewModifier {
enum ValidatorType: String {
case decimal = "^[-]?[\\d]*(?:\\###decimalSeparator###?[\\d]*)?$"
case number = "^\\d+$"
}
@Binding var goalValue: String
var validatorType: ValidatorType
private func validator(newValue: String) {
let regex: String = validatorType.rawValue.replacingOccurrences(of: "###decimalSeparator###", with: Locale.current.decimalSeparator!)
if newValue.range(of: regex, options: .regularExpression) != nil {
self.goalValue = newValue
} else if !self.goalValue.isEmpty {
self.goalValue = String(newValue.prefix(self.goalValue.count - 1))
}
}
func body(content: Content) -> some View {
content
.onReceive(Just(goalValue), perform: validator)
}
}
extension TextField {
func validator(goalValue: Binding<String>, type: TextFieldValidator.ValidatorType) -> some View {
modifier(TextFieldValidator(goalValue: goalValue, validatorType: type))
}
}
编号示例:
@State private var goalValue = "0"
TextField("1", text: $goalValue)
.validator(goalValue: $goalValue, type: .number)
.keyboardType(.numberPad)
十进制示例:
@State private var goalValue = "0,0"
TextField("1.0", text: $goalValue)
.validator(goalValue: $goalValue, type: .decimal)
.keyboardType(.decimalPad)
Jamone 将 Philip Pegden 的方法用于更强大的 NumericTextField 为我们提供了很好的服务。但是,如果在可滚动列表中使用 NumericTextField 并且部分滚动到视图之外,我发现该方法会出现一个问题。字符串的内部状态可能会丢失,从而导致滚动时出现意外行为。我还希望能够输入负数和指数部分(如 -1.6E-19 之类的数字)。我创建了一个新的 NumericTextField,它允许小数点、指数和仅包含字符串的减号选项。我还制作了一个从 onEditingChanged 错误条件触发的重新格式化函数。我的版本运行良好,但仍然可以使用更多的测试和改进。由于部分输入的数字会立即创建更新,因此部分输入通常不是 t 个数字并从数字转换器返回 nil。在转换失败时删除字符串的最后一个字符并重试似乎很简单,直到返回一个数字或没有剩余字符,在这种情况下返回 nil。一般来说,这将是最后输入的好数字。
如果在更改时发生大量计算,最好等到完成编辑后再绑定,但这不是正确的文本字段,就像最初在帖子顶部的点一样。无论如何,这是迄今为止我的版本的代码。
//String+Numeric.swift
import Foundation
public extension String {
/// Get the numeric only value from the string
/// - Parameter allowDecimalSeparator: If `true` then a single decimal separator will be allowed in the string's mantissa.
/// - Parameter allowMinusSign: If `true` then a single minus sign will be allowed at the beginning of the string.
/// - Parameter allowExponent: If `true` then a single e or E separator will be allowed in the string to start the exponent which can be a positive or negative integer
/// - Returns: Only numeric characters and optionally a single decimal character and optional an E followed by numeric characters.
/// If non-numeric values were interspersed `1a2b` then the result will be `12`.
/// The numeric characters returned may not be valid numbers so conversions will generally be optional strings.
func numericValue(allowDecimalSeparator: Bool = true, allowNegatives: Bool = true, allowExponent: Bool = true) -> String {
// Change parameters to single enum ?
var hasFoundDecimal = false
var allowMinusSign = allowNegatives // - can only be first char or first char after E (or e)
var hasFoundExponent = !allowExponent
var allowFindingExponent = false // initially false to avoid E as first character and then to prevent finding 2nd E
return self.filter {
if allowMinusSign && "-".contains($0){
return true
} else {
allowMinusSign = false
if $0.isWholeNumber {
allowFindingExponent = true
return true
} else if allowDecimalSeparator && String($0) == (Locale.current.decimalSeparator ?? ".") {
defer { hasFoundDecimal = true }
return !hasFoundDecimal
} else if allowExponent && !hasFoundExponent && allowFindingExponent && "eE".contains($0) {
allowMinusSign = true
hasFoundDecimal = true
allowFindingExponent = false
hasFoundExponent = true
return true
}
}
return false
}
}
此扩展允许带有减号和一个 E 或 e 的字符串,但只能在正确的位置。
然后 NumericTextModifier a la Jamone 是
//NumericTextModifier.swift
import SwiftUI
/// A modifier that observes any changes to a string, and updates that string to remove any non-numeric characters.
/// It also will convert that string to a `NSNumber` for easy use.
public struct NumericTextModifier: ViewModifier {
/// Should the user be allowed to enter a decimal number, or an integer
public let isDecimalAllowed: Bool
public let isExponentAllowed: Bool
public let isMinusAllowed: Bool
/// The string that the text field is bound to
/// A number that will be updated when the `text` is updated.
@Binding public var number: String
/// - Parameters:
/// - number:: The string 'number" that this should observe and filter
/// - isDecimalAllowed: Should the user be allowed to enter a decimal number, or an integer
/// - isExponentAllowed: Should the E (or e) be allowed in number for exponent entry
/// - isMinusAllowed: Should negatives be allowed with minus sign (-) at start of number
public init( number: Binding<String>, isDecimalAllowed: Bool, isExponentAllowed: Bool, isMinusAllowed: Bool) {
_number = number
self.isDecimalAllowed = isDecimalAllowed
self.isExponentAllowed = isExponentAllowed
self.isMinusAllowed = isMinusAllowed
}
public func body(content: Content) -> some View {
content
.onChange(of: number) { newValue in
let numeric = newValue.numericValue(allowDecimalSeparator: isDecimalAllowed, allowNegatives: isMinusAllowed, allowExponent: isExponentAllowed).uppercased()
if newValue != numeric {
number = numeric
}
}
}
}
public extension View {
/// A modifier that observes any changes to a string, and updates that string to remove any non-numeric characters.
func numericText(number: Binding<String>, isDecimalAllowed: Bool, isMinusAllowed: Bool, isExponentAllowed: Bool) -> some View {
modifier(NumericTextModifier( number: number, isDecimalAllowed: isDecimalAllowed, isExponentAllowed: isExponentAllowed, isMinusAllowed: isMinusAllowed))
}
}
NumericTextField 然后变为:
// NumericTextField.swift
import SwiftUI
/// A `TextField` replacement that limits user input to numbers.
public struct NumericTextField: View {
/// This is what consumers of the text field will access
@Binding private var numericText: String
private let isDecimalAllowed: Bool
private let isExponentAllowed: Bool
private let isMinusAllowed: Bool
private let title: LocalizedStringKey
//private let formatter: NumberFormatter
private let onEditingChanged: (Bool) -> Void
private let onCommit: () -> Void
/// Creates a text field with a text label generated from a localized title string.
///
/// - Parameters:
/// - titleKey: The key for the localized title of the text field,
/// describing its purpose.
/// - numericText: The number to be displayed and edited.
/// - isDecimalAllowed: Should the user be allowed to enter a decimal number, or an integer
/// - isExponentAllowed:Should the user be allowed to enter a e or E exponent character
/// - isMinusAllowed:Should user be allow to enter negative numbers
/// - formatter: NumberFormatter to use on getting focus or losing focus used by on EditingChanged
/// - onEditingChanged: An action thats called when the user begins editing `text` and after the user finishes editing `text`.
/// The closure receives a Boolean indicating whether the text field is currently being edited.
/// - onCommit: An action to perform when the user performs an action (for example, when the user hits the return key) while the text field has focus.
public init(_ titleKey: LocalizedStringKey, numericText: Binding<String>, isDecimalAllowed: Bool = true,
isExponentAllowed: Bool = true,
isMinusAllowed: Bool = true,
onEditingChanged: @escaping (Bool) -> Void = { _ in },
onCommit: @escaping () -> Void = {}) {
_numericText = numericText
self.isDecimalAllowed = isDecimalAllowed || isExponentAllowed
self.isExponentAllowed = isExponentAllowed
self.isMinusAllowed = isMinusAllowed
title = titleKey
self.onEditingChanged = onEditingChanged
self.onCommit = onCommit
}
public var body: some View {
TextField(title, text: $numericText,
onEditingChanged: { exited in
if !exited {
numericText = reformat(numericText)
}
onEditingChanged(exited)},
onCommit: {
numericText = reformat(numericText)
onCommit() })
.onAppear { numericText = reformat(numericText) }
.numericText( number: $numericText, isDecimalAllowed: isDecimalAllowed, isMinusAllowed: isMinusAllowed, isExponentAllowed: isExponentAllowed )
//.modifier(KeyboardModifier(isDecimalAllowed: isDecimalAllowed))
}
}
func reformat(_ stringValue: String) -> String {
if let value = NumberFormatter().number(from: stringValue) {
let compare = value.compare(NSNumber(0.0))
if compare == .orderedSame {
return "0"
}
if (compare == .orderedAscending) { // value negative
let compare = value.compare(NSNumber(-1e-3))
if compare != .orderedDescending {
let compare = value.compare(NSNumber(-1e5))
if compare == .orderedDescending {
return value.stringValue
}
}
}
else {
let compare = value.compare(NSNumber(1e5))
if compare == .orderedAscending {
let compare = value.compare(NSNumber(1e-3))
if compare != .orderedAscending {
return value.stringValue
}
}
}
return value.scientificStyle
}
return stringValue
}
private struct KeyboardModifier: ViewModifier {
let isDecimalAllowed: Bool
func body(content: Content) -> some View {
#if os(iOS)
return content
.keyboardType(isDecimalAllowed ? .decimalPad : .numberPad)
#else
return content
#endif
}
}
我直接使用了 func reformat(String) -> String 而不是格式化程序。Reformat 使用了几个格式化程序,至少对我来说更灵活。
import Foundation
var decimalNumberFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.allowsFloats = true
return formatter
}()
var scientificFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .scientific
formatter.allowsFloats = true
return formatter
}()
extension NSNumber {
var scientificStyle: String {
return scientificFormatter.string(from: self) ?? description
}
}
我希望其中一些可以帮助其他想要在他们的应用程序中使用科学记数法和负数的人。
快乐编码。
扩展John M.
示例以仅接受一个句点.
或一个逗号,
作为国际小数。
谢谢约翰 M。
struct TextFieldCharacterRestrictions: View {
@State private var numOfPeople = ""
var body: some View {
TextField("Total number of people", text: $numOfPeople)
.keyboardType(.decimalPad)
.onChange(of: numOfPeople){newValue in
let periodCount = newValue.components(separatedBy: ".").count - 1
let commaCount = newValue.components(separatedBy: ",").count - 1
if newValue.last == "." && periodCount > 1 || newValue.last == "," && commaCount > 1{
//it's a second period or comma, remove it
numOfPeople = String(newValue.dropLast())
// as bonus for the user, add haptic effect
let generator = UINotificationFeedbackGenerator()
generator.prepare()
generator.notificationOccurred(.warning)
}else{
let filtered = newValue.filter { "0123456789.,".contains($0) }
if filtered != newValue{
self.numOfPeople = filtered
}
}
}
}
}
更改文本:-> 值:并添加格式修饰符。
现在你可以处理你需要的一切。我会这样做:
TextField("Total Number of people:", value: $numOfPeople, format:.number)
.keyboardType(.numberPad)
这应该可以解决 99% 的问题。您可以在其中键入字符串,但它们会被过滤掉并且不会使您的应用程序崩溃。
这里作为基于 John M解决方案的变体,它避免了组合,支持任何值类型,并允许验证输出值,因此它只使用可解析和验证的输入字符串。
示例用法,保持边界值 > 0:
@State var count: Int
…
GenericEntryField(value: $count, validate: { $0 > 0 })
struct GenericEntryField<T: Equatable>: View {
@Binding var value: T
let stringToValue: (String) -> T?
let validate: (T) -> Bool
@State private var enteredText: String = ""
var body: some View {
return TextField(
"",
text: $enteredText,
onEditingChanged: { focussed in
if !focussed {
// when the textField is defocussed, reset the text back to the bound value
enteredText = "\(self.value)"
}
}
)
.onChange(of: enteredText) { newText in
// whenever the text-field changes, try to convert it to a value, and validate it.
// if so, use it (this will update the enteredText)
if let newValue = stringToValue(newText),
validate(newValue) {
self.value = newValue
}
}
.onChange(of: value) { newValue in
// whenever value changes externally, update the string
enteredText = "\(newValue)"
}
.onAppear(perform: {
// update the string based on value at start
enteredText = "\(value)"
})
}
}
extension GenericEntryField {
init(value: Binding<Int>, validate: @escaping (Int) -> Bool = { _ in true }) where T == Int {
self.init(value: value, stringToValue: { Int($0) }, validate: validate)
}
init(value: Binding<Double>, validate: @escaping (Double) -> Bool = { _ in true }) where T == Double {
self.init(value: value, stringToValue: { Double($0) }, validate: validate)
}
}
PositiveNumbersTextField 受到此处所写内容的极大启发(谢谢!)我想出了一个稍微不同的解决方案,以满足我的需求,并使用 .onChange 修饰符回答了上面的原始问题。文本字段将只输入允许 1 个小数点、0 或空的正数。消毒剂将删除多余的小数点、开始时的多个零、开始时的小数以及任何不是数字的字符(除了 1 个小数)。这不支持负数 (-)。
struct PositiveNumbersTextField: View {
@Binding var textFieldText: String
var body: some View {
TextField("", text: $textFieldText)
.keyboardType(.decimalPad)
.onChange(of: textFieldText) { text in
textFieldText = text.sanitizeToValidPositiveNumberOrEmpty()
}
}
}
private extension String {
func sanitizeToValidPositiveNumberOrEmpty() -> String {
var sanitized: String
// Remove multiple decimal points except the first one if exists.
let groups = self.components(separatedBy: ".")
if groups.count > 1 {
sanitized = groups[0] + "." + groups.dropFirst().joined()
} else {
sanitized = self
}
// Remove characters that are not numbers or decimal point
sanitized = sanitized.filter { $0.isNumber || $0 == "." }
// Don't allow decimal point at start
if sanitized.first == "." {
sanitized.removeFirst()
}
// Remove any number after 0 (if first number is zero)
if sanitized.first == "0" {
var stringIndicesToRemove = [String.Index]()
for index in 1..<sanitized.count {
let stringIndex = sanitized.index(sanitized.startIndex, offsetBy: index)
if sanitized[stringIndex] == "." {
break // no need to iterate through anymore
}
stringIndicesToRemove.append(stringIndex)
}
for stringIndexToRemove in stringIndicesToRemove.reversed() {
sanitized.remove(at: stringIndexToRemove)
}
}
return sanitized
}
}
仅接受数字的 TextField:
textField("", text: Binding(
get: {inputNum},
set: {inputNum = $0.filter{"0123456789".contains($0)}}))
.textFieldStyle(RoundedBorderTextFieldStyle())
将数字输入转换为 Int:
let n: Int = NumberFormatter().number(from: "0" + inputNum) as! Int