如何创建只接受数字的 TextField
How to create TextField that only accepts numbers
我是 SwiftUI 和 iOS 的新手,我正在尝试创建一个只接受数字的输入字段。
TextField("Total number of people", text: $numOfPeople)
TextField
目前允许输入字母字符,如何让用户只能输入数字?
tl;博士
查看 John M's 以获得更好的方法。
一种方法是您可以在 TextField
上设置键盘类型,这将限制人们可以输入的内容。
TextField("Total number of people", text: $numOfPeople)
.keyboardType(.numberPad)
可以找到 Apple 的文档 here, and you can see a list of all supported keyboard types here。
但是,此方法只是第一步,作为唯一的解决方案并不理想:
- iPad 没有数字小键盘,因此此方法不适用于 iPad.
- 如果用户使用的是硬件键盘,则此方法无效。
- 它不检查用户输入的内容。用户可以 copy/paste 将非数字值输入 TextField。
您应该清理输入的数据并确保它是纯数字。
对于执行该检查的解决方案 John M's solution 。他很好地解释了如何清理数据及其工作原理。
尽管显示数字键盘是良好的开端,但它实际上并不能防止输入错误数据:
- 用户可以将非数字文本粘贴到
TextField
- iPad 用户仍将获得全键盘
- 任何连接了蓝牙键盘的人都可以输入任何内容
您真正想要做的是清理输入,如下所示:
import SwiftUI
import Combine
struct WhosebugTests: 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([=10=]) }
if filtered != newValue {
self.numOfPeople = filtered
}
}
}
}
每当 numOfPeople
改变时,非数字值被过滤掉,过滤后的值被比较以查看 numOfPeople
是否应该第二次更新,用过滤后的错误输入覆盖输入。
请注意,Just
发布者要求您 import Combine
。
编辑:
为了解释 Just
发布者,请考虑以下关于更改 TextField
中的值时发生的情况的概念性概述:
- 因为
TextField
在字段内容更改时将 Binding
转换为 String
,它还会将更改写回 @State
变量。
- 当标记为
@State
的变量发生变化时,SwiftUI 会重新计算视图的 body
属性。
- 在
body
计算期间,创建了一个 Just
发布者。随着时间的推移,Combine 有很多不同的发布者来发布值,但是 Just
发布者“只”接受一个值(numberOfPeople
的新值)并在被询问时发布它。
onReceive
方法使 View
成为发布者的订阅者,在本例中为我们刚刚创建的 Just
发布者。一旦订阅,它会立即向发布者请求任何可用值,其中只有一个,新值 numberOfPeople
.
- 当
onReceive
订阅者收到一个值时,它会执行指定的闭包。我们的关闭可以以两种方式之一结束。如果文本已经只是数字,则它什么都不做。如果过滤后的文本不同,则将其写入 @State
变量,这将再次开始循环,但这次闭包将在不修改任何属性的情况下执行。
查看 Using Combine 了解更多信息。
你不需要使用Combine
和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 = [=10=].filter { "0123456789".contains([=10=]) } }))
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(Model())
}
}
不幸的是,还有一个小的闪烁,所以你也可以在很短的时间内看到不允许的字符(在我看来,Combine
的方式有点短)
另一种方法可能是创建一个包含 TextField 视图并包含两个值的视图:一个包含输入的字符串的私有变量,以及一个包含等效的 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)
}
}
}
这是一个简单的示例 - 您可能想要添加功能以显示输入不当的警告,可能还需要边界检查等...
@John M. 的 的 ViewModifier
版本。
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([=10=]) }
if filtered != newValue {
self.text = filtered
}
}
}
}
大多数答案都有一些明显的缺点。恕我直言,Philip 的 是迄今为止最好的。大多数其他答案不会在键入时过滤掉非数字字符。相反,您必须等到用户完成编辑后,他们才会更新文本以删除非数字字符。那么下一个常见问题是,当输入语言不使用 ASCII 0-9 字符作为数字时,它们不处理数字。
我想出了一个类似于 Philip 的解决方案,但它更适合生产。 NumericText SPM Package
首先,您需要一种从字符串中正确过滤非数字字符的方法,该方法适用于 unicode。
public extension String {
func numericValue(allowDecimalSeparator: Bool) -> String {
var hasFoundDecimal = false
return self.filter {
if [=10=].isWholeNumber {
return true
} else if allowDecimalSeparator && String([=10=]) == (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
}
}
先post到这里,如有错误请见谅。在我当前的项目中,我一直在努力解决这个问题。许多答案都很好,但仅适用于特定问题,就我而言,none 满足所有要求。
具体来说,我需要:
- 多个
Text
字段中的纯数字用户输入,包括负数。
- 将该输入绑定到来自 ObservableObject class 的 Double 类型的 var,以用于多个计算。
John M 的解决方案很棒,但它绑定到一个字符串形式的 @State 私有变量。
jamone 的回答和他的 NumericText 解决方案在很多方面都很棒,我在我的项目的 iOS14 版本中实现了它们。不幸的是,它不允许输入负数。
我提出的解决方案主要基于 John M 的回答,但结合了我从 jamone 的 NumericText 代码中学到的 onEditingChanged 的使用。这允许我根据 John M 的解决方案清理用户输入文本,然后(使用 onEditingChanged 调用的闭包)将该字符串绑定到一个 Observable Object Double。
所以我在下面的内容中确实没有什么新内容,对于更有经验的开发人员来说可能是显而易见的。但是在我所有的搜索中我从来没有偶然发现这个解决方案,所以我 post 在这里以防它帮助其他人。
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([=10=])}
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([=10=]) }
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)"
})
}
}
受到 John M.'s 的极大启发,我稍微修改了一些内容。
对我来说,在 Xcode 12 和 iOS 14,我注意到输入字母 did 显示在 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
;这会将基础变量的值设置为 everything-but-the-most-recent-character.
同样值得注意的是,如果您希望允许小数而不是仅限于整数,您可以将正则表达式字符串更改为 "^[\d]+\.?[\d]+$"
,您必须将其转义为 "^[\d]+\.?[\d]+$"
.
我认为更简单的方法是使用 自定义绑定 并将任何字符串直接转换为数值。通过这种方式,您还可以将 State 变量作为 number 而不是 string,这是一个巨大的 IMO。
以下是所有需要的代码。请注意,在无法转换字符串的情况下使用默认值(在这种情况下为零)。
@State private var myValue: Int
// ...
TextField("number", text: Binding(
get: { String(myValue) },
set: { myValue = Int([=10=]) ?? 0 }
))
可以将 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 无法格式化的文本,则该值将不会更改。因此,这可能会或可能不会涵盖您的问题“仅接受数字的文本字段”。
Jamone 将 Philip Pegden 的方法用于更强大的 NumericTextField,为我们提供了很好的服务。但是,如果在可滚动列表中使用 NumericTextField 并且部分滚动出视图,我会发现该方法存在一个问题。字符串的内部状态可能会丢失,从而导致滚动时出现意外行为。我还希望能够输入负数和指数部分(如 -1.6E-19 的数字)。我制作了一个新的 NumericTextField,它允许小数点、指数和仅包含字符串的减号选项。我还制作了一个从 onEditingChanged false 条件触发的重新格式化函数。我的版本运行良好,但仍可以进行更多测试和改进。由于部分输入的数字会立即创建更新,因此部分条目通常不是数字,并且来自数字转换器的 return nil。在转换失败时删除字符串的最后一个字符并重试直到数字被 returned 或没有更多字符留下,在这种情况下 nil 被 returned 似乎是直截了当的。一般来说,这将是最后输入的好号码。
如果更改发生大量计算,最好等到完成编辑后再绑定,但这不是正确的文本字段,因为最初位于 post.无论如何,这是我目前版本的代码。
//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([=10=]){
return true
} else {
allowMinusSign = false
if [=10=].isWholeNumber {
allowFindingExponent = true
return true
} else if allowDecimalSeparator && String([=10=]) == (Locale.current.decimalSeparator ?? ".") {
defer { hasFoundDecimal = true }
return !hasFoundDecimal
} else if allowExponent && !hasFoundExponent && allowFindingExponent && "eE".contains([=10=]) {
allowMinusSign = true
hasFoundDecimal = true
allowFindingExponent = false
hasFoundExponent = true
return true
}
}
return false
}
}
此扩展允许带有减号和一个 E 或 e 的字符串,但仅限于正确的位置。
那么 Jamone 的 NumericTextModifier 是
//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
}
}
我希望其中的一些内容可以帮助其他想要在其应用中使用科学记数法和负数的人。
编码愉快。
您也可以使用简单的 formatter:
struct AView: View {
@State var numberValue:Float
var body: some View {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
return TextField("number", value: $numberValue, formatter: NumberFormatter())
}
用户仍然可以尝试输入一些文本,如下所示:
但是格式化程序强制使用一个数字。
PositiveNumbersTextField
深受这里所写内容的启发(谢谢大家!)我想出了一个稍微不同的解决方案来满足我的需要并使用 .onChange 修饰符回答上面的原始问题。
文本字段将仅允许 1 个小数点、0 或空的正数输入除外。
消毒剂将删除多余的小数点、开头的多个零、开头的小数点和任何非数字字符(小数点后一位除外)。
这不支持负数 (-)。
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 { [=10=].isNumber || [=10=] == "." }
// 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
}
}
@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)
更改文本:-> 值:并添加格式修饰符。
现在您可以处理您需要的一切。我会选择这个:
TextField("Total Number of people:", value: $numOfPeople, format:.number)
.keyboardType(.numberPad)
这应该可以解决 99% 的问题。您可以在其中键入字符串,但它们会被过滤掉并且不会使您的应用程序崩溃。
这里是基于 John M 的 的变体,它避免了 Combine,支持任何值类型,并允许验证输出值,因此它只使用可解析和验证的输入字符串.
示例用法,保持绑定值 > 0:
@State var count: Int
…
GenericEntryField(value: $count, validate: { [=10=] > 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([=11=]) }, validate: validate)
}
init(value: Binding<Double>, validate: @escaping (Double) -> Bool = { _ in true }) where T == Double {
self.init(value: value, stringToValue: { Double([=11=]) }, validate: validate)
}
}
仅接受数字的文本字段:
textField("", text: Binding(
get: {inputNum},
set: {inputNum = [=10=].filter{"0123456789".contains([=10=])}}))
.textFieldStyle(RoundedBorderTextFieldStyle())
将数字输入转换为 Int:
let n: Int = NumberFormatter().number(from: "0" + inputNum) as! Int
扩展 John M.
的示例以仅接受一个句点 .
或一个逗号 ,
用于国际小数。
谢谢 John 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([=10=]) }
if filtered != newValue{
self.numOfPeople = filtered
}
}
}
}
}
我根据答案做了一个扩展,你所要做的就是在你的项目中添加以下代码:
import SwiftUI
import Combine
struct TextFieldSanitize: ViewModifier {
@Binding private var text: String
private let allowedChars: String
init(text: Binding<String>, allowedChars: String) {
self.allowedChars = allowedChars
self._text = text
}
func body(content: Content) -> some View {
content
.onReceive(Just(text)) { newValue in
let filtered = newValue.filter { Set(allowedChars).contains([=10=]) }
if filtered != newValue { text = filtered }
}
}
}
extension View {
func onlyAcceptingAllowedChars(_ allowedChars: String, in text: Binding<String>) -> some View {
modifier(TextFieldSanitize(text: text, allowedChars: allowedChars))
}
func onlyAcceptingDouble(in text: Binding<String>) -> some View {
let decimalSeparator = Locale.current.decimalSeparator ?? "."
let allowedChars = "0123456789\(decimalSeparator)"
return onlyAcceptingAllowedChars(allowedChars, in: text)
}
func onlyAcceptingInt(in text: Binding<String>) -> some View {
let allowedChars = "0123456789"
return onlyAcceptingAllowedChars(allowedChars, in: text)
}
}
用法:
如果你想创建一个只接受整数的TextField,你可以按照下面的例子:
import SwiftUI
struct WhosebugTests: View {
@State private var numOfPeople = "0"
var body: some View {
TextField("Total number of people", text: $numOfPeople)
.keyboardType(.numberPad)
.onlyAcceptingInt(in: $numOfPeople)
}
}
可以通过使用 onlyAcceptingDouble
方法对 Double
执行相同的操作。
如果您想制作自定义消毒剂,例如只接受“A”、“2”和“%”作为字符的 TextField,只需像这样调用 onlyAcceptingAllowedChars
方法:
import SwiftUI
struct WhosebugTests: View {
@State private var customText = ""
var body: some View {
TextField("Custom text", text: $customText)
.onlyAcceptingAllowedChars("A2%", in: $customText)
}
}
此答案已在以 iOS 14 为目标的项目中进行测试。
这个解决方案对我来说效果很好。一旦提交,它会自动将其格式化为数字,如果需要,您可以添加自己的自定义验证 - 在我的例子中,我想要最大值 100。
@State private var opacity = 100
TextField("Opacity", value: $opacity, format: .number)
.onChange(of: opacity) { newValue in
if newValue > 100 {
opacity = 100
}
}
.keyboardType(.numberPad)
.multilineTextAlignment(.center)
我是 SwiftUI 和 iOS 的新手,我正在尝试创建一个只接受数字的输入字段。
TextField("Total number of people", text: $numOfPeople)
TextField
目前允许输入字母字符,如何让用户只能输入数字?
tl;博士
查看 John M's
一种方法是您可以在 TextField
上设置键盘类型,这将限制人们可以输入的内容。
TextField("Total number of people", text: $numOfPeople)
.keyboardType(.numberPad)
可以找到 Apple 的文档 here, and you can see a list of all supported keyboard types here。
但是,此方法只是第一步,作为唯一的解决方案并不理想:
- iPad 没有数字小键盘,因此此方法不适用于 iPad.
- 如果用户使用的是硬件键盘,则此方法无效。
- 它不检查用户输入的内容。用户可以 copy/paste 将非数字值输入 TextField。
您应该清理输入的数据并确保它是纯数字。
对于执行该检查的解决方案 John M's solution
尽管显示数字键盘是良好的开端,但它实际上并不能防止输入错误数据:
- 用户可以将非数字文本粘贴到
TextField
- iPad 用户仍将获得全键盘
- 任何连接了蓝牙键盘的人都可以输入任何内容
您真正想要做的是清理输入,如下所示:
import SwiftUI
import Combine
struct WhosebugTests: 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([=10=]) }
if filtered != newValue {
self.numOfPeople = filtered
}
}
}
}
每当 numOfPeople
改变时,非数字值被过滤掉,过滤后的值被比较以查看 numOfPeople
是否应该第二次更新,用过滤后的错误输入覆盖输入。
请注意,Just
发布者要求您 import Combine
。
编辑:
为了解释 Just
发布者,请考虑以下关于更改 TextField
中的值时发生的情况的概念性概述:
- 因为
TextField
在字段内容更改时将Binding
转换为String
,它还会将更改写回@State
变量。 - 当标记为
@State
的变量发生变化时,SwiftUI 会重新计算视图的body
属性。 - 在
body
计算期间,创建了一个Just
发布者。随着时间的推移,Combine 有很多不同的发布者来发布值,但是Just
发布者“只”接受一个值(numberOfPeople
的新值)并在被询问时发布它。 onReceive
方法使View
成为发布者的订阅者,在本例中为我们刚刚创建的Just
发布者。一旦订阅,它会立即向发布者请求任何可用值,其中只有一个,新值numberOfPeople
.- 当
onReceive
订阅者收到一个值时,它会执行指定的闭包。我们的关闭可以以两种方式之一结束。如果文本已经只是数字,则它什么都不做。如果过滤后的文本不同,则将其写入@State
变量,这将再次开始循环,但这次闭包将在不修改任何属性的情况下执行。
查看 Using Combine 了解更多信息。
你不需要使用Combine
和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 = [=10=].filter { "0123456789".contains([=10=]) } }))
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(Model())
}
}
不幸的是,还有一个小的闪烁,所以你也可以在很短的时间内看到不允许的字符(在我看来,Combine
的方式有点短)
另一种方法可能是创建一个包含 TextField 视图并包含两个值的视图:一个包含输入的字符串的私有变量,以及一个包含等效的 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)
}
}
}
这是一个简单的示例 - 您可能想要添加功能以显示输入不当的警告,可能还需要边界检查等...
@John M. 的 ViewModifier
版本。
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([=10=]) }
if filtered != newValue {
self.text = filtered
}
}
}
}
大多数答案都有一些明显的缺点。恕我直言,Philip 的
我想出了一个类似于 Philip 的解决方案,但它更适合生产。 NumericText SPM Package
首先,您需要一种从字符串中正确过滤非数字字符的方法,该方法适用于 unicode。
public extension String {
func numericValue(allowDecimalSeparator: Bool) -> String {
var hasFoundDecimal = false
return self.filter {
if [=10=].isWholeNumber {
return true
} else if allowDecimalSeparator && String([=10=]) == (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
}
}
先post到这里,如有错误请见谅。在我当前的项目中,我一直在努力解决这个问题。许多答案都很好,但仅适用于特定问题,就我而言,none 满足所有要求。
具体来说,我需要:
- 多个
Text
字段中的纯数字用户输入,包括负数。 - 将该输入绑定到来自 ObservableObject class 的 Double 类型的 var,以用于多个计算。
John M 的解决方案很棒,但它绑定到一个字符串形式的 @State 私有变量。
jamone 的回答和他的 NumericText 解决方案在很多方面都很棒,我在我的项目的 iOS14 版本中实现了它们。不幸的是,它不允许输入负数。
我提出的解决方案主要基于 John M 的回答,但结合了我从 jamone 的 NumericText 代码中学到的 onEditingChanged 的使用。这允许我根据 John M 的解决方案清理用户输入文本,然后(使用 onEditingChanged 调用的闭包)将该字符串绑定到一个 Observable Object Double。
所以我在下面的内容中确实没有什么新内容,对于更有经验的开发人员来说可能是显而易见的。但是在我所有的搜索中我从来没有偶然发现这个解决方案,所以我 post 在这里以防它帮助其他人。
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([=10=])}
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([=10=]) } 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)" }) } }
受到 John M.'s
对我来说,在 Xcode 12 和 iOS 14,我注意到输入字母 did 显示在 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
;这会将基础变量的值设置为 everything-but-the-most-recent-character.
同样值得注意的是,如果您希望允许小数而不是仅限于整数,您可以将正则表达式字符串更改为 "^[\d]+\.?[\d]+$"
,您必须将其转义为 "^[\d]+\.?[\d]+$"
.
我认为更简单的方法是使用 自定义绑定 并将任何字符串直接转换为数值。通过这种方式,您还可以将 State 变量作为 number 而不是 string,这是一个巨大的 IMO。
以下是所有需要的代码。请注意,在无法转换字符串的情况下使用默认值(在这种情况下为零)。
@State private var myValue: Int
// ...
TextField("number", text: Binding(
get: { String(myValue) },
set: { myValue = Int([=10=]) ?? 0 }
))
可以将 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 无法格式化的文本,则该值将不会更改。因此,这可能会或可能不会涵盖您的问题“仅接受数字的文本字段”。
Jamone 将 Philip Pegden 的方法用于更强大的 NumericTextField,为我们提供了很好的服务。但是,如果在可滚动列表中使用 NumericTextField 并且部分滚动出视图,我会发现该方法存在一个问题。字符串的内部状态可能会丢失,从而导致滚动时出现意外行为。我还希望能够输入负数和指数部分(如 -1.6E-19 的数字)。我制作了一个新的 NumericTextField,它允许小数点、指数和仅包含字符串的减号选项。我还制作了一个从 onEditingChanged false 条件触发的重新格式化函数。我的版本运行良好,但仍可以进行更多测试和改进。由于部分输入的数字会立即创建更新,因此部分条目通常不是数字,并且来自数字转换器的 return nil。在转换失败时删除字符串的最后一个字符并重试直到数字被 returned 或没有更多字符留下,在这种情况下 nil 被 returned 似乎是直截了当的。一般来说,这将是最后输入的好号码。
如果更改发生大量计算,最好等到完成编辑后再绑定,但这不是正确的文本字段,因为最初位于 post.无论如何,这是我目前版本的代码。
//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([=10=]){
return true
} else {
allowMinusSign = false
if [=10=].isWholeNumber {
allowFindingExponent = true
return true
} else if allowDecimalSeparator && String([=10=]) == (Locale.current.decimalSeparator ?? ".") {
defer { hasFoundDecimal = true }
return !hasFoundDecimal
} else if allowExponent && !hasFoundExponent && allowFindingExponent && "eE".contains([=10=]) {
allowMinusSign = true
hasFoundDecimal = true
allowFindingExponent = false
hasFoundExponent = true
return true
}
}
return false
}
}
此扩展允许带有减号和一个 E 或 e 的字符串,但仅限于正确的位置。
那么 Jamone 的 NumericTextModifier 是
//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
}
}
我希望其中的一些内容可以帮助其他想要在其应用中使用科学记数法和负数的人。
编码愉快。
您也可以使用简单的 formatter:
struct AView: View {
@State var numberValue:Float
var body: some View {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
return TextField("number", value: $numberValue, formatter: NumberFormatter())
}
用户仍然可以尝试输入一些文本,如下所示:
但是格式化程序强制使用一个数字。
PositiveNumbersTextField 深受这里所写内容的启发(谢谢大家!)我想出了一个稍微不同的解决方案来满足我的需要并使用 .onChange 修饰符回答上面的原始问题。 文本字段将仅允许 1 个小数点、0 或空的正数输入除外。 消毒剂将删除多余的小数点、开头的多个零、开头的小数点和任何非数字字符(小数点后一位除外)。 这不支持负数 (-)。
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 { [=10=].isNumber || [=10=] == "." }
// 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
}
}
@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)
更改文本:-> 值:并添加格式修饰符。
现在您可以处理您需要的一切。我会选择这个:
TextField("Total Number of people:", value: $numOfPeople, format:.number)
.keyboardType(.numberPad)
这应该可以解决 99% 的问题。您可以在其中键入字符串,但它们会被过滤掉并且不会使您的应用程序崩溃。
这里是基于 John M 的
示例用法,保持绑定值 > 0:
@State var count: Int
…
GenericEntryField(value: $count, validate: { [=10=] > 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([=11=]) }, validate: validate)
}
init(value: Binding<Double>, validate: @escaping (Double) -> Bool = { _ in true }) where T == Double {
self.init(value: value, stringToValue: { Double([=11=]) }, validate: validate)
}
}
仅接受数字的文本字段:
textField("", text: Binding(
get: {inputNum},
set: {inputNum = [=10=].filter{"0123456789".contains([=10=])}}))
.textFieldStyle(RoundedBorderTextFieldStyle())
将数字输入转换为 Int:
let n: Int = NumberFormatter().number(from: "0" + inputNum) as! Int
扩展 John M.
的示例以仅接受一个句点 .
或一个逗号 ,
用于国际小数。
谢谢 John 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([=10=]) }
if filtered != newValue{
self.numOfPeople = filtered
}
}
}
}
}
我根据
import SwiftUI
import Combine
struct TextFieldSanitize: ViewModifier {
@Binding private var text: String
private let allowedChars: String
init(text: Binding<String>, allowedChars: String) {
self.allowedChars = allowedChars
self._text = text
}
func body(content: Content) -> some View {
content
.onReceive(Just(text)) { newValue in
let filtered = newValue.filter { Set(allowedChars).contains([=10=]) }
if filtered != newValue { text = filtered }
}
}
}
extension View {
func onlyAcceptingAllowedChars(_ allowedChars: String, in text: Binding<String>) -> some View {
modifier(TextFieldSanitize(text: text, allowedChars: allowedChars))
}
func onlyAcceptingDouble(in text: Binding<String>) -> some View {
let decimalSeparator = Locale.current.decimalSeparator ?? "."
let allowedChars = "0123456789\(decimalSeparator)"
return onlyAcceptingAllowedChars(allowedChars, in: text)
}
func onlyAcceptingInt(in text: Binding<String>) -> some View {
let allowedChars = "0123456789"
return onlyAcceptingAllowedChars(allowedChars, in: text)
}
}
用法:
如果你想创建一个只接受整数的TextField,你可以按照下面的例子:
import SwiftUI
struct WhosebugTests: View {
@State private var numOfPeople = "0"
var body: some View {
TextField("Total number of people", text: $numOfPeople)
.keyboardType(.numberPad)
.onlyAcceptingInt(in: $numOfPeople)
}
}
可以通过使用 onlyAcceptingDouble
方法对 Double
执行相同的操作。
如果您想制作自定义消毒剂,例如只接受“A”、“2”和“%”作为字符的 TextField,只需像这样调用 onlyAcceptingAllowedChars
方法:
import SwiftUI
struct WhosebugTests: View {
@State private var customText = ""
var body: some View {
TextField("Custom text", text: $customText)
.onlyAcceptingAllowedChars("A2%", in: $customText)
}
}
此答案已在以 iOS 14 为目标的项目中进行测试。
这个解决方案对我来说效果很好。一旦提交,它会自动将其格式化为数字,如果需要,您可以添加自己的自定义验证 - 在我的例子中,我想要最大值 100。
@State private var opacity = 100
TextField("Opacity", value: $opacity, format: .number)
.onChange(of: opacity) { newValue in
if newValue > 100 {
opacity = 100
}
}
.keyboardType(.numberPad)
.multilineTextAlignment(.center)