Swift UI如何创建仅接受数字的TextField

时间:2019-11-06 15:01:47

标签: swiftui swiftui-form

我是swiftUI和iO的新手,我试图创建一个仅接受数字的输入字段

 TextField("Total number of people", text: $numOfPeople)

TextField还允许使用字母字符,如何限制用户仅输入数字?

13 个答案:

答案 0 :(得分:9)

虽然显示数字键盘是很好的第一步,但实际上并不能阻止输入不良数据:

  1. 用户可以将非数字文本粘贴到文本字段中
  2. iPad用户仍将拥有完整的键盘
  3. 连接了蓝牙键盘的任何人都可以键入任何内容

您真正想要做的是清除输入,例如:

import SwiftUI
import Combine

struct StackOverflowTests: View {
    @State private var numOfPeople = "0"

    var body: some View {
        TextField("Total number of people", text: $numOfPeople)
            .keyboardType(.numberPad)
            .onReceive(Just(numOfPeople)) { newValue in
                let filtered = newValue.filter { "0123456789".contains($0) }
                if filtered != newValue {
                    self.numOfPeople = filtered
                }
        }
    }
}

只要numOfPeople发生变化,就会过滤掉非数字值,并比较过滤后的值以查看是否应再次更新numOfPeople,从而用过滤后的输入覆盖错误的输入。

请注意,Just发布者要求您import Combine

答案 1 :(得分:8)

在我看来,使用自定义绑定并直接将任何字符串转换为数值要容易得多。通过这种方式,您还可以将 State 变量作为 number 而不是 string,这是一个巨大的 IMO。

以下是所有需要的代码。请注意,如果字符串无法转换(在这种情况下为零),则使用默认值。

@State private var myValue: Int
// ...
TextField("number", text: Binding(
    get: { String(myValue) }, 
    set: { myValue = Int($0) ?? 0 }
))

答案 2 :(得分:4)

John M.answer的启发下,我做了些微修改。

对我来说,在Xcode 12和iOS 14上,我注意到TextField上显示的字母 did 会显示,尽管我不想这么做。我希望忽略字母,并且只允许 个数字。

这就是我所做的:

@State private var goalValue = ""

var body: some View {
    TextField("12345", text: self.$goalValue)
        .keyboardType(.numberPad)
        .onReceive(Just(self.goalValue), perform: self.numericValidator)
}

func numericValidator(newValue: String) {
    if newValue.range(of: "^\\d+$", options: .regularExpression) != nil {
        self.goalValue = newValue
    } else if !self.goalValue.isEmpty {
        self.goalValue = String(newValue.prefix(self.goalValue.count - 1))
    }
}

这里的关键是else if;这会将基础变量的值设置为所有,但最新的字符。

同样值得注意的是,如果您想允许使用十进制数字而不是仅限于整数,则可以将正则表达式字符串更改为"^[\d]+\.?[\d]+$",必须将其转义为"^[\\d]+\\.?[\\d]+$"

答案 3 :(得分:3)

您可以在TextField上设置键盘类型,这将限制人们可以输入的内容。

TextField("Total number of people", text: $numOfPeople)
    .keyboardType(.numberPad)

可以找到here Apple的文档,您可以看到所有受支持的键盘类型的列表here

答案 4 :(得分:3)

另一种方法可能是创建一个包装TextField视图的View,并保存两个值:一个保存输入的String的私有var和一个保存Double等效项的可绑定值。每次用户键入字符时,它都会尝试更新Double。

这是一个基本的实现:

struct NumberEntryField : View {
    @State private var enteredValue : String = ""
    @Binding var value : Double

    var body: some View {        
        return TextField("", text: $enteredValue)
            .onReceive(Just(enteredValue)) { typedValue in
                if let newValue = Double(typedValue) {
                    self.value = newValue
                }
        }.onAppear(perform:{self.enteredValue = "\(self.value)"})
    }
}

您可以这样使用它:

struct MyView : View {
    @State var doubleValue : Double = 1.56

    var body: some View {        
        return HStack {
             Text("Numeric field:")
             NumberEntryField(value: self.$doubleValue)   
            }
      }
}

这是一个简单的示例-您可能想要添加功能以显示警告,提示输入不当以及边界检查等...

答案 5 :(得分:3)

@John M.的answerViewModifier版本。

import Combine
import SwiftUI

public struct NumberOnlyViewModifier: ViewModifier {

    @Binding var text: String

    public init(text: Binding<String>) {
        self._text = text
    }

    public func body(content: Content) -> some View {
        content
            .keyboardType(.numberPad)
            .onReceive(Just(text)) { newValue in
                let filtered = newValue.filter { "0123456789".contains($0) }
                if filtered != newValue {
                    self.text = filtered
                }
            }
    }
}

答案 6 :(得分:3)

大多数答案都有一些明显的缺点。 Philip的answer是迄今为止最好的恕我直言。其他大多数答案都不会在键入非数字字符时过滤掉它们。取而代之的是,您必须等到用户完成编辑后,他们才能更新文本以删除非数字字符。然后,下一个常见的问题是,当输入语言不使用ASCII 0-9字符作为数字时,它们不处理数字。

我想出了一个与Philip相似的解决方案,但该产品已经可以投入生产。 NumericText SPM Package

首先,您需要一种从字符串中正确过滤非数字字符的方法,该方法可与unicode一起正常工作。

public extension String {
    func numericValue(allowDecimalSeparator: Bool) -> String {
        var hasFoundDecimal = false
        return self.filter {
            if $0.isWholeNumber {
                return true
            } else if allowDecimalSeparator && String($0) == (Locale.current.decimalSeparator ?? ".") {
                defer { hasFoundDecimal = true }
                return !hasFoundDecimal
            }
            return false
        }
    }
}

然后将文本字段包装在新视图中。我希望我可以将所有这些作为修饰符。虽然我可以将字符串过滤为一个,但您失去了文本字段绑定数字值的能力。

public struct NumericTextField: View {

    @Binding private var number: NSNumber?
    @State private var string: String
    private let isDecimalAllowed: Bool
    private let formatter: NumberFormatter = NumberFormatter()

    private let title: LocalizedStringKey
    private let onEditingChanged: (Bool) -> Void
    private let onCommit: () -> Void

    public init(_ titleKey: LocalizedStringKey, number: Binding<NSNumber?>, isDecimalAllowed: Bool, onEditingChanged: @escaping (Bool) -> Void = { _ in }, onCommit: @escaping () -> Void = {}) {
        formatter.numberStyle = .decimal
        _number = number
        if let number = number.wrappedValue, let string = formatter.string(from: number) {
            _string = State(initialValue: string)
        } else {
            _string = State(initialValue: "")
        }
        self.isDecimalAllowed = isDecimalAllowed
        title = titleKey
        self.onEditingChanged = onEditingChanged
        self.onCommit = onCommit
    }

    public var body: some View {
        return TextField(title, text: $string, onEditingChanged: onEditingChanged, onCommit: onCommit)
            .onChange(of: string, perform: numberChanged(newValue:))
            .modifier(KeyboardModifier(isDecimalAllowed: isDecimalAllowed))
    }

    private func numberChanged(newValue: String) {
        let numeric = newValue.numericValue(allowDecimalSeparator: isDecimalAllowed)
        if newValue != numeric {
            string = numeric
        }
        number = formatter.number(from: string)
    }
}

您严格不需要此修饰符,但似乎您几乎总是想要它。

private struct KeyboardModifier: ViewModifier {
    let isDecimalAllowed: Bool

    func body(content: Content) -> some View {
        #if os(iOS)
            return content
                .keyboardType(isDecimalAllowed ? .decimalPad : .numberPad)
        #else
            return content
        #endif
    }
}

答案 7 :(得分:3)

可以将 NumberFormatter 交给 TextField 并让它为您处理转换:

$detailsQuery = mysqli_query($conn, "SELECT details FROM detailsTable WHERE id IN (".implode(',', $IDvalues).")");

请注意,一旦用户完成编辑,就会应用格式化程序。如果用户输入的文本不能被 NumberFormatter 格式化,则该值不会改变。因此,这可能会也可能不会涵盖您的问题“仅接受数字的文本字段”。

答案 8 :(得分:2)

首先在这里发布,因此请原谅任何错误。我在当前的项目中一直在为这个问题而苦苦挣扎。许多答案都很有效,但仅针对特定问题,就我而言,没有一个满足所有要求。

我特别需要:

  1. 多个文本字段中的仅数字用户输入,包括负数。
  2. 将该输入绑定到ObservableObject类的Double类型的var中,以用于多次计算。

John M的解决方案很棒,但是它可以绑定到@State私有var,它是一个字符串。

jamone的答案,他的NumericText解决方案在许多方面都很棒,我在项目的iOS14版本中实现了它。不幸的是,它不允许输入负数。

我想出的解决方案主要基于John M的答案,但结合了我从jamone的NumericText代码中学到的onEditingChanged的用法。这使我可以根据John M的解决方案来清理用户输入的文本,然后(使用onEditingChanged调用闭包)将该字符串绑定到Observable Object Double。

因此,我在下面的内容中没有什么新鲜的东西,对于更有经验的开发人员来说可能很明显。但是在我所有的搜索中,我从未偶然发现过该解决方案,因此我将其发布在这里,以防它对其他人有所帮助。

import Foundation
import Combine

class YourData: ObservableObject {
    @Published var number = 0
}

func convertString(string: String) -> Double {
    guard let doubleString = Double(string) else { return 0 }
    return doubleString
}

struct ContentView: View {

    @State private var input = ""
    @EnvironmentObject var data: YourData

    var body: some View { 
        
        TextField("Enter string", text: $input, onEditingChanged: { 
            _ in self.data.number = convertString(string: self.input) })
            .keyboardType(.numbersAndPunctuation)

            .onReceive(Just(input)) { cleanNum in
                let filtered = cleanNum.filter {"0123456789.-".contains($0)}
                if filtered != cleanNum {
                    self.input = filtered
                }
            }
        }
}

答案 9 :(得分:1)

您不需要使用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 = $0.filter { "0123456789".contains($0) } }))
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView().environmentObject(Model())
    }
}

不幸的是,还有一个小的闪烁,因此您也可以在很短的时间内看到非允许的字符(在我眼中,使用Combine时会更短)

答案 10 :(得分:1)

我建议基于@John M.和@hstdt的版本处理:

  • 以绑定值开头

  • 负数

  • 十进制分隔符(如果不止一个,请剪切字符串)

    struct NumberField : View {
    
      @Binding var value : Double
      @State private var enteredValue = "#START#"
    
      var body: some View {
          return TextField("", text: $enteredValue)
              .onReceive(Just(enteredValue)) { typedValue in
                  var typedValue_ = typedValue == "#START#" ? String(self.value) : typedValue
                  if typedValue != "" {
                      let negative = typedValue_.hasPrefix("-") ? "-" : ""
                      typedValue_ = typedValue_.filter { "0123456789.".contains($0) }
                      let parts = typedValue_.split(separator: ".")
                      let formatedValue = parts.count == 1 ? negative + String(parts[0]) : negative + String(parts[0]) + "." + String(parts[1])
                      self.enteredValue = formatedValue
                  }
                  let newValue = Double(self.enteredValue) ?? 0.0
                  self.value = newValue
    
          }
          .onAppear(perform:{
              self.enteredValue = "\(self.value)"
          })
      }
    }
    

答案 11 :(得分:1)

Jamone 采用 Philip Pegden 的方法来构建更强大的 NumericTextField,为我们提供了很好的服务。但是,如果在可滚动列表中使用 NumericTextField 并且部分滚动到视图之外,我发现该方法会出现一个问题。字符串的内部状态可能会丢失,从而导致滚动时出现意外行为。我还希望能够输入负数和指数部分(例如 -1.6E-19 之类的数字)。我创建了一个新的 NumericTextField,它允许选择小数点、指数和仅包含字符串的减号。我还制作了一个从 onEditingChanged false 条件触发的重新格式化函数。我的版本运行良好,但仍然可以使用更多的测试和改进。由于部分输入的数字会立即创建更新,因此部分条目通常不是数字并从数字转换器返回 nil。似乎在转换失败时删除字符串的最后一个字符并重试直到返回一个数字或没有更多字符剩余,在这种情况下返回 nil 似乎很简单。通常,这将是输入的最后一个有效数字。

如果在更改时进行了大量计算,最好等到编辑完成后再绑定,但这不是正确的文本字段,因为最初位于帖子顶部的点也是如此。无论如何,这是迄今为止我的版本的代码。

    //String+Numeric.swift
    import Foundation

    public extension String {
        /// Get the numeric only value from the string
        /// - Parameter allowDecimalSeparator: If `true` then a single decimal separator will be allowed in the string's mantissa.
        /// - Parameter allowMinusSign: If `true` then a single minus sign will be allowed at the beginning of the string.
        /// - Parameter allowExponent: If `true` then a single e or E  separator will be allowed in the string to start the exponent which can be a positive or negative integer
        /// - Returns: Only numeric characters and optionally a single decimal character and optional an E followed by numeric characters.
        ///            If non-numeric values were interspersed `1a2b` then the result will be `12`.
        ///            The numeric characters returned may not be valid numbers so conversions will generally be optional strings.

func numericValue(allowDecimalSeparator: Bool = true, allowNegatives: Bool = true, allowExponent: Bool = true) -> String {
    // Change parameters to single enum ?
    var hasFoundDecimal = false
    var allowMinusSign = allowNegatives // - can only be first char or first char after E (or e)
    var hasFoundExponent = !allowExponent
    var allowFindingExponent = false // initially false to avoid E as first character and then to prevent finding 2nd E
    return self.filter {
        if allowMinusSign && "-".contains($0){
            return true
        } else {
            allowMinusSign = false
            if $0.isWholeNumber {
                allowFindingExponent = true
              return true
           } else if allowDecimalSeparator && String($0) == (Locale.current.decimalSeparator ?? ".") {
              defer { hasFoundDecimal = true }
              return !hasFoundDecimal
           } else if allowExponent && !hasFoundExponent && allowFindingExponent && "eE".contains($0) {
              allowMinusSign = true
              hasFoundDecimal = true
              allowFindingExponent = false
              hasFoundExponent = true
              return true
           }
        }
        return false
    }
}

此扩展允许带有减号和一个 E 或 e 的字符串,但仅限于正确的位置。

那么 NumericTextModifier a la Jamone 是

    //NumericTextModifier.swift
    import SwiftUI
    /// A modifier that observes any changes to a string, and updates that string to remove any non-numeric characters.
    /// It also will convert that string to a `NSNumber` for easy use.
    public struct NumericTextModifier: ViewModifier {
        /// Should the user be allowed to enter a decimal number, or an integer
        public let isDecimalAllowed: Bool
        public let isExponentAllowed: Bool
        public let isMinusAllowed: Bool
        /// The string that the text field is bound to
        /// A number that will be updated when the `text` is updated.
        @Binding public var number: String
        /// - Parameters:
        ///   - number:: The string 'number" that this should observe and filter
        ///   - isDecimalAllowed: Should the user be allowed to enter a decimal number, or an integer
        ///   - isExponentAllowed: Should the E (or e) be allowed in number for exponent entry
        ///   - isMinusAllowed: Should negatives be allowed with minus sign (-) at start of number
        public init( number: Binding<String>, isDecimalAllowed: Bool, isExponentAllowed: Bool, isMinusAllowed: Bool) {
            _number = number
            self.isDecimalAllowed = isDecimalAllowed
            self.isExponentAllowed = isExponentAllowed
            self.isMinusAllowed = isMinusAllowed
        }
        public func body(content: Content) -> some View {
            content
                .onChange(of: number) { newValue in
                    let numeric = newValue.numericValue(allowDecimalSeparator: isDecimalAllowed, allowNegatives: isMinusAllowed, allowExponent: isExponentAllowed).uppercased()
                    if newValue != numeric {
                        number = numeric
                    }
                }
        }
    }

    public extension View {
        /// A modifier that observes any changes to a string, and updates that string to remove any non-numeric characters.
        func numericText(number: Binding<String>, isDecimalAllowed: Bool, isMinusAllowed: Bool, isExponentAllowed: Bool) -> some View {
            modifier(NumericTextModifier( number: number, isDecimalAllowed: isDecimalAllowed, isExponentAllowed: isExponentAllowed, isMinusAllowed: isMinusAllowed))
        }
    }

NumericTextField 然后变成:

    // NumericTextField.swift
    import SwiftUI

    /// A `TextField` replacement that limits user input to numbers.
    public struct NumericTextField: View {

        /// This is what consumers of the text field will access
        @Binding private var numericText: String
    
        private let isDecimalAllowed: Bool
        private let isExponentAllowed: Bool
        private let isMinusAllowed: Bool
        
        private let title: LocalizedStringKey
        //private let formatter: NumberFormatter
        private let onEditingChanged: (Bool) -> Void
        private let onCommit: () -> Void


        /// Creates a text field with a text label generated from a localized title string.
        ///
        /// - Parameters:
        ///   - titleKey: The key for the localized title of the text field,
        ///     describing its purpose.
        ///   - numericText: The number to be displayed and edited.
        ///   - isDecimalAllowed: Should the user be allowed to enter a decimal number, or an integer
        ///   - isExponentAllowed:Should the user be allowed to enter a e or E exponent character
        ///   - isMinusAllowed:Should user be allow to enter negative numbers
        ///   - formatter: NumberFormatter to use on getting focus or losing focus used by on EditingChanged
        ///   - onEditingChanged: An action thats called when the user begins editing `text` and after the user finishes editing `text`.
        ///     The closure receives a Boolean indicating whether the text field is currently being edited.
        ///   - onCommit: An action to perform when the user performs an action (for example, when the user hits the return key) while the text field has focus.
        public init(_ titleKey: LocalizedStringKey, numericText: Binding<String>, isDecimalAllowed: Bool = true,
            isExponentAllowed: Bool = true,
            isMinusAllowed: Bool = true,
           
            onEditingChanged: @escaping (Bool) -> Void = { _ in  },
            onCommit: @escaping () -> Void = {}) {
                _numericText = numericText
           
                self.isDecimalAllowed = isDecimalAllowed || isExponentAllowed
                self.isExponentAllowed = isExponentAllowed
                self.isMinusAllowed = isMinusAllowed
                title = titleKey
                self.onEditingChanged = onEditingChanged
                self.onCommit = onCommit
        }
        
        
        public var body: some View {
            TextField(title, text: $numericText,
                onEditingChanged: { exited in
                    if !exited {
                        numericText = reformat(numericText)
                    }
                    onEditingChanged(exited)},
                onCommit: {
                    numericText = reformat(numericText)
                    onCommit() })
                .onAppear { numericText = reformat(numericText) }
                .numericText( number: $numericText, isDecimalAllowed: isDecimalAllowed, isMinusAllowed: isMinusAllowed, isExponentAllowed: isExponentAllowed )
                //.modifier(KeyboardModifier(isDecimalAllowed: isDecimalAllowed))
           
        }
    }

    func reformat(_ stringValue: String) -> String {
        if let value = NumberFormatter().number(from: stringValue) {
            let compare = value.compare(NSNumber(0.0))
                if compare == .orderedSame {
                    return "0"
                }
                if (compare == .orderedAscending) { // value negative
                    let compare = value.compare(NSNumber(-1e-3))
                    if compare != .orderedDescending {
                        let compare = value.compare(NSNumber(-1e5))
                        if compare == .orderedDescending {
                            return value.stringValue
                        }
                    }
                }
                else {
                    let compare = value.compare(NSNumber(1e5))
                    if compare == .orderedAscending {
                        let compare = value.compare(NSNumber(1e-3))
                        if compare != .orderedAscending {
                            return value.stringValue
                        }
                    }
                }
                return value.scientificStyle
        }
        return stringValue
    }

    private struct KeyboardModifier: ViewModifier {
        let isDecimalAllowed: Bool

        func body(content: Content) -> some View {
            #if os(iOS)
            return content
                .keyboardType(isDecimalAllowed ? .decimalPad : .numberPad)
            #else
            return content
            #endif
        }
    }

我直接使用了 func reformat(String) -> String 而不是格式化程序。重新格式化使用了几个格式化程序,至少对我来说更灵活。

    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
        }
    }

我希望其中的一些内容可以帮助那些想要在他们的应用中使用科学记数法和负数的人。

快乐编码。

答案 12 :(得分:0)

您也可以使用简单的 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())
}

用户仍然可以尝试输入一些文本,如下所示:

demo

但是格式化程序强制使用一个数字。