如何创建只接受数字的 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

但是,此方法只是第一步,作为唯一的解决方案并不理想

  1. iPad 没有数字小键盘,因此此方法不适用于 iPad.
  2. 如果用户使用的是硬件键盘,则此方法无效。
  3. 它不检查用户输入的内容。用户可以 copy/paste 将非数字值输入 TextField。

您应该清理输入的数据并确保它是纯数字。

对于执行该检查的解决方案 John M's solution 。他很好地解释了如何清理数据及其工作原理。

尽管显示数字键盘是良好的开端,但它实际上并不能防止输入错误数据:

  1. 用户可以将非数字文本粘贴到 TextField
  2. iPad 用户仍将获得全键盘
  3. 任何连接了蓝牙键盘的人都可以输入任何内容

您真正想要做的是清理输入,如下所示:

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 中的值时发生的情况的概念性概述:

  1. 因为 TextField 在字段内容更改时将 Binding 转换为 String,它还会将更改写回 @State 变量。
  2. 当标记为 @State 的变量发生变化时,SwiftUI 会重新计算视图的 body 属性。
  3. body 计算期间,创建了一个 Just 发布者。随着时间的推移,Combine 有很多不同的发布者来发布值,但是 Just 发布者“只”接受一个值(numberOfPeople 的新值)并在被询问时发布它。
  4. onReceive 方法使 View 成为发布者的订阅者,在本例中为我们刚刚创建的 Just 发布者。一旦订阅,它会立即向发布者请求任何可用值,其中只有一个,新值 numberOfPeople.
  5. onReceive 订阅者收到一个值时,它会执行指定的闭包。我们的关闭可以以两种方式之一结束。如果文本已经只是数字,则它什么都不做。如果过滤后的文本不同,则将其写入 @State 变量,这将再次开始循环,但这次闭包将在不修改任何属性的情况下执行。

查看 Using Combine 了解更多信息。

你不需要使用CombineonReceive,你也可以使用这个代码:

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 满足所有要求。

具体来说,我需要:

  1. 多个 Text 字段中的纯数字用户输入,包括负数。
  2. 将该输入绑定到来自 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)