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

Lup*_*ati 2 swiftui swiftui-form

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

 TextField("Total number of people", text: $numOfPeople)
Run Code Online (Sandbox Code Playgroud)

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

And*_*rew 42

tl;博士

查看John M解决方案以获得更好的方法。


一种方法是您可以设置键盘类型,TextField这将限制人们可以输入的内容。

TextField("Total number of people", text: $numOfPeople)
    .keyboardType(.numberPad)
Run Code Online (Sandbox Code Playgroud)

苹果的文档,可以发现在这里,你可以看到所有支持的键盘类型的列表在这里

但是,这种方法只是第一步,并不是唯一的解决方案:

  1. iPad 没有数字键盘,因此此方法不适用于 iPad。
  2. 如果用户使用的是硬件键盘,则此方法将不起作用。
  3. 它不会检查用户输入的内容。用户可以将非数字值复制/粘贴到 TextField 中。

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

对于执行该检查的解决方案,请查看下面的John M解决方案。他在解释如何清理数据及其工作原理方面做得很好。

  • 这实际上并不会阻止非数字输入;看我的回答。 (10认同)

Ber*_*rik 24

可以将 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())
    }
}
Run Code Online (Sandbox Code Playgroud)

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


hst*_*tdt 12

ViewModifier@John M.'s answer的版本。

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

Run Code Online (Sandbox Code Playgroud)


len*_*nny 11

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

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

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


小智 9

通过重启发约翰M答案,我修改的东西咯。

对我来说,在 Xcode 12 和 iOS 14 上,我注意到键入字母确实显示在 中TextField,尽管我不希望它们显示。我希望忽略字母,允许使用数字。

这是我所做的:

@State private var goalValue = ""

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

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

这里的关键是else if; 这将基础变量的值设置为除最新字符之外的所有内容。

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

  • 凯西对 SO 有五个答案,我在听 ATP 时偶然发现了其中一个。有趣的!顺便说一句:有帮助,谢谢! (2认同)

Phi*_*den 7

另一种方法可能是创建一个包含 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)"})
    }
}
Run Code Online (Sandbox Code Playgroud)

你可以这样使用它:

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

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

这是一个简单的示例 - 您可能想要添加功能来显示输入不良的警告,也许还需要边界检查等......


Ake*_*ian 7

您还可以使用简单的格式化程序

struct AView: View {
    @State var numberValue:Float
    var body: some View {
        let formatter = NumberFormatter()
        formatter.numberStyle = .decimal
        return TextField("number", value: $numberValue, formatter: NumberFormatter())
}

Run Code Online (Sandbox Code Playgroud)

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

演示

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


Joh*_* M. 6

尽管显示数字键盘是很好的第一步,但实际上并不能阻止输入错误的数据:

  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
                }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

每当numOfPeople更改时,都会过滤掉非数字值,并比较过滤后的值以查看是否numOfPeople应该第二次更新,从而用过滤后的输入覆盖错误的输入。

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

  • 优秀的解决方案,就像魅力一样,谢谢!提示:为了提高性能,请将引用的数字包装为“Set”,例如“Set("0123456789").contains($0)”。通过这个小升级,包含函数现在可以使用常量“O(1)”,而不是线性“O(n)”复杂度。它虽小,但很有用的改进。 (9认同)
  • 该解决方案会导致循环问题,因为它改变了观察到的属性的值。好的解决方案是使用 TextField 的“formatter:”参数? (2认同)
  • @tedtanner 看起来你是对的。我在 Google Benchmark 中尝试了这个过滤器,纯字符串比 Set 快得多,即使 contains 函数具有 **O(1)** 复杂性。当然是在同一根弦上进行测试。对于我两年前的评论,我向大家表示歉意。:-/ **测试:纯字符串 1792.000 ns** **测试:设置字符串 13750.000 ns** (2认同)

jam*_*one 6

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

我想出了一个类似于菲利普的解决方案,但它更适合生产。NumericText SPM 包

首先,您需要一种从字符串中正确过滤非数字字符的方法,该方法适用于 unicode。

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

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

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)
    }
}
Run Code Online (Sandbox Code Playgroud)

你并不严格需要这个修饰符,但你似乎总是想要它。

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
    }
}
Run Code Online (Sandbox Code Playgroud)


小智 5

第一次发帖,如有错误还请多多包涵。我在当前的项目中一直在努力解决这个问题。许多答案都很好,但仅适用于特定问题,就我而言,没有一个答案满足所有要求。

具体来说,我需要:

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

John M 的解决方案很棒,但它绑定到一个字符串形式的 @State 私有变量。

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

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

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

import Foundation
import Combine

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

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

struct ContentView: View {

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

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

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