SwiftUI - Как создать TextField, который принимает только числа
Я новичок в SwiftUI и iOS, и я пытаюсь создать поле ввода, которое будет принимать только числа.
TextField("Total number of people", text: $numOfPeople)
В TextField
в настоящее время разрешены буквенные символы, как мне сделать так, чтобы пользователь мог вводить только числа?
29 ответов
tl;dr
Ознакомьтесь с решением John M., чтобы найти лучший способ.
Один из способов сделать это - установить тип клавиатуры на TextField
что ограничит то, что люди могут печатать.
TextField("Total number of people", text: $numOfPeople)
.keyboardType(.numberPad)
Документацию Apple можно найти здесь, и вы можете увидеть список всех поддерживаемых типов клавиатуры здесь.
Однако этот метод - только первый шаг и не идеален как единственное решение:
- iPad не имеет цифровой панели, поэтому этот метод не будет работать на iPad.
- Если пользователь использует аппаратную клавиатуру, этот метод не сработает.
- Он не проверяет, что ввел пользователь. Пользователь может скопировать / вставить нечисловое значение в TextField.
Вам следует очистить вводимые данные и убедиться, что они являются чисто числовыми.
Для решения, которое делает это, ознакомьтесь с решением John M. ниже. Он отлично объясняет, как дезинфицировать данные и как это работает.
Хотя отображение цифровой клавиатуры - хороший первый шаг, на самом деле это не препятствует вводу неверных данных:
- Пользователь может вставлять нечисловой текст в текстовое поле
- Пользователи iPad по-прежнему получат полноценную клавиатуру
- Любой, у кого есть Bluetooth-клавиатура, может печатать что угодно
Что вы действительно хотите сделать, так это очистить ввод, например:
import SwiftUI
import Combine
struct StackruTests: 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
.
РЕДАКТИРОВАТЬ:
Чтобы объяснить Just
издатель, рассмотрите следующую концептуальную схему того, что происходит, когда вы меняете значение в TextField
:
- Потому как
TextField
занимаетBinding
кString
, когда содержимое поля изменяется, оно также записывает это изменение обратно в@State
переменная. - Когда переменная отмечена
@State
изменений, SwiftUI пересчитываетbody
свойство представления. - Вовремя
body
вычисление, aJust
издатель создан. У Combine есть много разных издателей, которые будут выдавать значения с течением времени, ноJust
издатель принимает "только" одно значение (новое значениеnumberOfPeople
) и издает его, когда его спросят. - В
onReceive
метод делаетView
подписчик на издателя, в данном случаеJust
издатель, которого мы только что создали. После подписки он сразу же запрашивает у издателя любые доступные значения, из которых есть только одно, новое значениеnumberOfPeople
. - Когда
onReceive
подписчик получает значение, он выполняет указанное закрытие. Наше закрытие может закончиться одним из двух способов. Если текст уже числовой, он ничего не делает. Если отфильтрованный текст отличается, он записывается в@State
переменная, которая снова начинает цикл, но на этот раз закрытие выполняется без изменения каких-либо свойств.
Проверьте Использование Combine для получения дополнительной информации.
На мой взгляд, намного проще использовать привязку и сразу же преобразовывать любые строки в числовые значения. Таким образом, у вас также есть переменная State в виде числа вместо строки, что является огромным плюсом IMO.
Ниже приведен весь необходимый код. Обратите внимание, что значение по умолчанию используется в случае, если строка не может быть преобразована (в данном случае ноль).
@State private var myValue: Int
// ...
TextField("number", text: Binding(
get: {String(myValue)},
set: {v in myValue = Int(v) ?? 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, значение не будет изменено. Таким образом, это может или не может охватывать ваш вопрос «текстовое поле, которое принимает только числа».
В ViewModifier
версия @John Мандельштама ответ.
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
}
}
}
}
Сильно вдохновлен John M.. "S ответ, я изменил вещи немного.
Для меня, на Xcode 12 и 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))
}
}
Ключевым моментом здесь является
else if
; это устанавливает значение базовой переменной как все, кроме самого последнего символа.
Также стоит отметить, что если вы хотите разрешить десятичные числа, а не ограничиваться только целыми числами, вы можете изменить строку регулярного выражения на
"^[\d]+\.?[\d]+$"
, которого вам придется сбежать, чтобы стать
"^[\\d]+\\.?[\\d]+$"
.
У большинства ответов есть существенные недостатки. Ответ Филиппа пока самый лучший ИМХО. Большинство других ответов не фильтруют нечисловые символы по мере их ввода. Вместо этого вам нужно подождать, пока пользователь закончит редактирование, а затем они обновят текст, удалив нечисловые символы. Затем следующая распространенная проблема заключается в том, что они не обрабатывают числа, когда язык ввода не использует символы ASCII 0-9 для чисел.
Я придумал решение, похожее на решение Филиппа, но оно больше готово к производству. Пакет NumericText SPM
Сначала вам нужен способ правильно отфильтровать нечисловые символы из строки, который правильно работает с юникодом.
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
}
}
Другой подход, возможно, заключается в создании представления, которое обертывает представление 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)
}
}
}
Это простой пример - вы можете захотеть добавить функциональность для отображения предупреждения о плохом вводе, и, возможно, проверки границ и т. Д.
Вы также можете использовать простой форматтер :
struct AView: View {
@State var numberValue:Float
var body: some View {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
return TextField("number", value: $numberValue, formatter: NumberFormatter())
}
Пользователи все еще могут попытаться ввести текст, как показано здесь:
Но форматер требует использования номера.
Первый пост здесь, прошу простить все ошибки. Я боролся с этим вопросом в моем текущем проекте. Многие ответы работают хорошо, но только для определенных проблем, и в моем случае ни один из них не отвечал всем требованиям.
В частности, мне понадобились:
- Пользовательский ввод только числовых значений, включая отрицательные числа, в несколько текстовых полей.
- Привязка этого ввода к переменной типа Double из класса ObservableObject для использования в нескольких вычислениях.
Решение John M великолепно, но оно привязывается к частной переменной @State, которая является строкой.
Ответ jamone, и его решение NumericText во многих отношениях фантастическое, и я реализовал его в версии своего проекта для iOS14. К сожалению, он не позволяет вводить отрицательные числа.
Решение, которое я придумал, было основано в основном на ответе Джона М., но включает использование onEditingChanged, которое я узнал из кода NumericText Джамона. Это позволяет мне очищать вводимый пользователем текст на основе решения Джона М, но затем (с закрытием, вызываемым 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
}
}
}
}
Вам не нужно использовать 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 = $0.filter { "0123456789".contains($0) } }))
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(Model())
}
}
К сожалению, есть также небольшое мерцание, поэтому вы также можете видеть запрещенные символы в течение очень короткого времени (на мой взгляд, немного короче, чем путь с Combine
)
ViewModifier @cliss отвечает с учетом десятичного разделителя для языка, установленного на устройстве. Не стесняйтесь расширять это решение:
// 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)
Расширение
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($0) }
if filtered != newValue{
self.numOfPeople = filtered
}
}
}
}
}
Улучшенный ответ
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 { $0.isNumber }
if filtered != newValue {
self.numOfPeople = filtered
}
}
}
}
В этой теме уже есть много решений, но ни одно из них не было многоразовым, универсальным и охватывало случай пустого текстового поля:
extension TextField where Label == Text {
init<V: LosslessStringConvertible>(_ titleKey: LocalizedStringKey, value: Binding<V?>) {
self.init(titleKey, text: Binding<String>(
get: {
if let value = value.wrappedValue{
return String(value)
} else {
return String()
}
}, set: { text in
if text.isEmpty {
value.wrappedValue = nil
} else if let v = V(text) {
value.wrappedValue = v
}
}
))
}
}
Использование:
struct ContentView: View {
@State var value: Int? = 0
var body: some View {
VStack {
TextField("Test", value: $value)
Text("The value is \(value ?? -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)" }) } }
Вы можете обрезать символы, которые не являются числами (используя .decimalDigits.inverted):
import SwiftUI
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
self.numOfPeople = newValue.trimmingCharacters(in: .decimalDigits.inverted)
}
}
}
Измените текст: -> значение: и добавьте модификатор формата.
Теперь вы можете справиться со всем, что вам нужно. Я бы просто пошел с этим:
TextField("Total Number of people:", value: $numOfPeople, format:.number)
.keyboardType(.numberPad)
Это должно быть хорошо для 99% ваших проблем. Вы можете ввести там строки, но они будут отфильтрованы и не приведут к сбою вашего приложения.
Джамон, который воспользовался подходом Филипа Пегдена к более надежному NumericTextField, оказал нам большую услугу. Однако одна проблема, которую я обнаружил в этом подходе, возникает, если NumericTextField используется в прокручиваемом списке, а часть прокручивается вне поля зрения. Внутреннее состояние строки может быть потеряно, что приведет к неожиданному поведению при прокрутке. Я также хотел иметь возможность вводить отрицательные числа и экспоненциальные части (например, -1.6E-19). Я создаю новый NumericTextField, который позволяет использовать десятичную точку, показатель степени и знак минус, который содержит только строку. Я также сделал функцию переформатирования, которая запускается из ложного условия onEditingChanged. Моя версия работает довольно хорошо, но все же требует дополнительных тестов и улучшений. Поскольку частично введенное число немедленно обновляется, частичные записи часто отсутствуют.t чисел и верните 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
}
}
Я использовал переформатирование функций (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
}
}
Я надеюсь, что это поможет другим, кто хочет использовать научную запись и отрицательные числа в своих приложениях.
Удачного кодирования.
Я сделал расширение на основе ответа Джона М , все, что вам нужно сделать, это добавить в свой проект следующий код:
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($0) }
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 StackOverflowTests: View {
@State private var numOfPeople = "0"
var body: some View {
TextField("Total number of people", text: $numOfPeople)
.keyboardType(.numberPad)
.onlyAcceptingInt(in: $numOfPeople)
}
}
То же самое можно сделать для
Double
с помощью
onlyAcceptingDouble
метод вместо этого.
Если вы хотите создать собственное дезинфицирующее средство, например TextField, которое принимает только символы «A», «2» и «%», просто вызовите
onlyAcceptingAllowedChars
такой метод:
import SwiftUI
struct StackOverflowTests: 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)
Вот что я придумал до того, как нашел эту тему. Пока он немного отличается от других...
func numericTextField(_ str: String, _ bstr: Binding<String>) -> some View {
return TextField("float", text: bstr)
.keyboardType(.numbersAndPunctuation)
.padding(8)
.background(SwiftUI.Color.gray)
.foregroundColor(Double(str) == nil ? Color.red : Color.white)
}
Здесь цвет текста записи становится красным, когда текущая строка не является допустимым числом с плавающей запятой. Это не мешает вам возвращать строку, которую нельзя разобрать на Double или что-то еще, но дает вам визуальную обратную связь. Я должен использовать правильные цвета предупреждений.
Строка, которую я передаю, является переменной @State, поэтому мне нужно было передать ее дважды, чтобы получить версию $string внутри функции. Может быть, есть более элегантный способ сделать это, но это сработало для меня. Строка используется для обновления двойника в коде перерисовки, если строка действительна. Используется для обновления номера. Звонок выглядит так...
numericTextField(str, $str)
Я использую клавиатуру .numbersAndPunctuation, потому что мне нужен знак минус, а иногда и буква «е» для показателей степени. Клавиатура немного больше, чем десятичная, но на экране она такого же размера, как и обычная клавиатура, поэтому макет страницы не скачет, если вы переключаетесь между редактированием строк и чисел.
Постскриптум:
Посмотрев на другие решения, вот моя новая версия NumberFormatter...
func doubleTextField(_ val: Binding<Double>) -> some View {
let fmt = NumberFormatter()
fmt.numberStyle = .decimal
return TextField("float", value: val, formatter: fmt)
.keyboardType(.numbersAndPunctuation)
.padding(8)
.background(SwiftUI.Color.gray)
Мне понравился текст с изменяющимся цветом, который давал понять, что вы набрали что-то неправильно, но это ближе к тому, что обычное TextField делает со строками.
Комплексное решение Swift UI
- TextField разрешает только числовое значение
- Должен принимать только одну запятую ("".")
- Ограничить десятичную точку до x десятичного знака
Файл NumbersOnlyViewModifier
import Foundation
import SwiftUI
import Combine
struct NumbersOnlyViewModifier: ViewModifier {
@Binding var text: String
var includeDecimal: Bool
var digitAllowedAfterDecimal: Int = 1
func body(content: Content) -> some View {
content
.keyboardType(includeDecimal ? .decimalPad : .numberPad)
.onReceive(Just(text)) { newValue in
var numbers = "0123456789"
let decimalSeparator: String = Locale.current.decimalSeparator ?? "."
if includeDecimal {
numbers += decimalSeparator
}
if newValue.components(separatedBy: decimalSeparator).count-1 > 1 {
let filtered = newValue
self.text = isValid(newValue: String(filtered.dropLast()), decimalSeparator: decimalSeparator)
} else {
let filtered = newValue.filter { numbers.contains($0)}
if filtered != newValue {
self.text = isValid(newValue: filtered, decimalSeparator: decimalSeparator)
} else {
self.text = isValid(newValue: newValue, decimalSeparator: decimalSeparator)
}
}
}
}
private func isValid(newValue: String, decimalSeparator: String) -> String {
guard includeDecimal, !text.isEmpty else { return newValue }
let component = newValue.components(separatedBy: decimalSeparator)
if component.count > 1 {
guard let last = component.last else { return newValue }
if last.count > digitAllowedAfterDecimal {
let filtered = newValue
return String(filtered.dropLast())
}
}
return newValue
}
}
Просмотр файла + расширение
extension View {
func numbersOnly(_ text: Binding<String>, includeDecimal: Bool = false) -> some View {
self.modifier(NumbersOnlyViewModifier(text: text, includeDecimal: includeDecimal))
}
}
File ViewFile
TextField("", text: $value, onEditingChanged: { isEditing in
self.isEditing = isEditing
})
.foregroundColor(Color.neutralGray900)
.numbersOnly($value, includeDecimal: true)
.font(.system(size: Constants.FontSizes.fontSize22))
.multilineTextAlignment(.center)
TextField, который принимает только числа:
textField("", text: Binding(
get: {inputNum},
set: {inputNum = $0.filter{"0123456789".contains($0)}}))
.textFieldStyle(RoundedBorderTextFieldStyle())
Преобразование числового ввода в Int:
let n: Int = NumberFormatter().number(from: "0" + inputNum) as! Int
import Combine
import SwiftUI
struct ContentView: View {
@State private var numOfPeople = ""
private let numberOnlyFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 0
return formatter
}()
var body: some View {
TextField("Number of People", text: $numOfPeople)
.keyboardType(.numberPad)
.textContentType(.oneTimeCode)
.onReceive(Just(numOfPeople)) { newValue in
let filtered = newValue.filter { "0123456789".contains($0) }
if filtered != newValue {
self.numOfPeople = filtered
}
}
.padding()
}
}
Это то, что я использую
Здесь как вариант, основанный на решении Джона М , который избегает комбинирования, поддерживает любой тип значения и позволяет проверять выходное значение, так что он использует входную строку только в том случае, если она поддается анализу и проверке.
Пример использования, который сохраняет связанное значение> 0:
@State var count: Int
…
GenericEntryField(value: $count, validate: { $0 > 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($0) }, validate: validate)
}
init(value: Binding<Double>, validate: @escaping (Double) -> Bool = { _ in true }) where T == Double {
self.init(value: value, stringToValue: { Double($0) }, validate: validate)
}
}
Самый простой способ TextField("Долгота", значение:$longitude, средство форматирования:NumberFormatter()).keyboardType(.numberPad)
PositiveNumbersTextField Во многом вдохновленный тем, что здесь было написано (спасибо всем!), Я придумал немного другое решение, которое соответствует моим потребностям и отвечает на исходный вопрос выше с использованием модификатора .onChange. В текстовом поле можно вводить только положительные числа, разрешающие 1 десятичную точку, 0 или пустое поле. Дезинфицирующее средство удалит лишние десятичные точки, несколько нулей в начале, десятичное число в начале и любой символ, не являющийся числом (кроме 1 десятичного знака). Это не поддерживает отрицательные числа (-).
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 { $0.isNumber || $0 == "." }
// Don't allow decimal point at start
if sanitized.first == "." {
sanitized.removeFirst()
}
// Remove multiple sequential zeros at start (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
}
if sanitized[stringIndex] == "0" {
stringIndicesToRemove.append(stringIndex)
}
}
for stringIndexToRemove in stringIndicesToRemove.reversed() {
sanitized.remove(at: stringIndexToRemove)
}
}
return sanitized
}
}