SwiftUI-如何创建只接受数字的文本字段
我不熟悉SwiftUI和iOS,我正在尝试创建一个只接受数字的输入字段SwiftUI-如何创建只接受数字的文本字段,swiftui,swiftui-form,Swiftui,Swiftui Form,我不熟悉SwiftUI和iOS,我正在尝试创建一个只接受数字的输入字段 TextField("Total number of people", text: $numOfPeople) TextField当前允许使用字母字符,如何使其仅允许用户输入数字?tl;博士 结帐是为了更好的方式 一种方法是,您可以在TextField上设置键盘类型,这将限制人们可以键入的内容 TextField("Total number of people", text: $numOfPeopl
TextField("Total number of people", text: $numOfPeople)
TextField
当前允许使用字母字符,如何使其仅允许用户输入数字?tl;博士
结帐是为了更好的方式
一种方法是,您可以在
TextField
上设置键盘类型,这将限制人们可以键入的内容
TextField("Total number of people", text: $numOfPeople)
.keyboardType(.numberPad)
苹果的文档可以找到,你可以看到所有支持的键盘类型的列表
但是,该方法只是第一步,作为唯一的解决方案,并不理想:
对于执行该签出的解决方案。他在解释如何清理数据及其工作原理方面做得很好。尽管显示数字键盘是一个好的第一步,但实际上并不能防止输入坏数据:
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
}
}
}
}
struct MyView : View {
@State var doubleValue : Double = 1.56
var body: some View {
return HStack {
Text("Numeric field:")
NumberEntryField(value: self.$doubleValue)
}
}
}
每当numfopeople
发生更改时,非数值将被过滤掉,并比较过滤后的值,以查看是否应再次更新numfopeople
,用过滤后的输入覆盖错误的输入
请注意,Just
发布服务器要求您导入联合收割机
编辑:
解释<代码>只是 Publisher,考虑下面的概念大纲,当在代码>文本字段< /代码>中更改值时发生什么:
TextField
将绑定到字符串
,因此当字段内容更改时,它还会将更改写回@State
变量
@State
的变量更改时,SwiftUI将重新计算视图的body
属性body
计算过程中,将创建一个Just
发布服务器。Combine有很多不同的发布者可以随时间发出值,但是Just
发布者只需要“Just”一个值(新值numberOfPeople
)并在被询问时发出onReceive
方法使视图成为发布者的订户,在本例中,就是我们刚刚创建的发布者。订阅后,它会立即向发布者请求任何可用值,其中只有一个值,即新值numberOfPeople
onReceive
订阅服务器接收到一个值时,它执行指定的闭包。我们的关闭可以以两种方式之一结束。如果文本已经只是数字,那么它什么也不做。如果过滤的文本不同,则会将其写入@State
变量,该变量将再次开始循环,但这次将在不修改任何属性的情况下执行闭包查看以了解更多信息。您不需要使用
组合
和接收
,也可以使用以下代码:
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())
}
}
不幸的是,还有一个小的闪烁,因此您也可以在很短的时间内看到不允许的字符(在我看来,这比使用
组合的方式要短一些)另一种方法可能是创建一个包装TextField视图的视图,并保存两个值:一个保存输入字符串的私有变量,和一个可绑定的值,该值保存双重等效值。每次用户键入一个字符时,它都会尝试更新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)"})
}
}
您可以这样使用它:
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
}
}
}
}
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
版本
导入联合收割机
导入快捷键
公共结构NumberOnlyViewModifier:ViewModifier{
@绑定变量文本:字符串
公共初始化(文本:绑定){
self.\u text=文本
}
公共函数体(内容:content)->一些视图{
内容
.键盘类型(.numberPad)
.onReceive(Just(text)){newValue in
让filtered=newValue.filter{“0123456789”。包含($0)}
如果已筛选!=newValue{
self.text=已过滤
}
}
}
}
大多数答案都有一些明显的缺点。菲利普的是迄今为止最好的。大多数其他答案在键入非数字字符时不会过滤掉这些字符。相反,您必须等到用户完成编辑之后,然后他们更新文本以删除非数字字符。然后,下一个常见问题是,当输入语言不使用ASCII 0-9字符作为数字时,它们不处理数字
我提出了一个类似于Philip的解决方案,但更适合生产
首先,您需要一种从字符串中正确过滤非数字字符的方法,这种方法可以与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)
}
}
首先在这里发布,所以请原谅任何错误。在我目前的项目中,我一直在努力解决这个问题。许多答案都很有效,但只适用于特定问题,就我而言,没有一个答案满足所有要求
具体而言,我需要:
仅数字用户输入,i
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
}
}
}
}
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)"
})
}
}
@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))
}
}
@State private var myValue: Int
// ...
TextField("number", text: Binding(
get: { String(myValue) },
set: { myValue = Int($0) ?? 0 }
))
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())
}
}
//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
}
}
//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.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
}
}
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
}
}