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 можно найти здесь, и вы можете увидеть список всех поддерживаемых типов клавиатуры здесь.

Однако этот метод - только первый шаг и не идеален как единственное решение:

  1. iPad не имеет цифровой панели, поэтому этот метод не будет работать на iPad.
  2. Если пользователь использует аппаратную клавиатуру, этот метод не сработает.
  3. Он не проверяет, что ввел пользователь. Пользователь может скопировать / вставить нечисловое значение в TextField.

Вам следует очистить вводимые данные и убедиться, что они являются чисто числовыми.

Для решения, которое делает это, ознакомьтесь с решением John M. ниже. Он отлично объясняет, как дезинфицировать данные и как это работает.

Хотя отображение цифровой клавиатуры - хороший первый шаг, на самом деле это не препятствует вводу неверных данных:

  1. Пользователь может вставлять нечисловой текст в текстовое поле
  2. Пользователи iPad по-прежнему получат полноценную клавиатуру
  3. Любой, у кого есть 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:

  1. Потому как TextField занимает Binding к String, когда содержимое поля изменяется, оно также записывает это изменение обратно в @State переменная.
  2. Когда переменная отмечена @State изменений, SwiftUI пересчитывает body свойство представления.
  3. Вовремя body вычисление, a Justиздатель создан. У Combine есть много разных издателей, которые будут выдавать значения с течением времени, ноJust издатель принимает "только" одно значение (новое значение numberOfPeople) и издает его, когда его спросят.
  4. В onReceive метод делает View подписчик на издателя, в данном случае Justиздатель, которого мы только что создали. После подписки он сразу же запрашивает у издателя любые доступные значения, из которых есть только одно, новое значениеnumberOfPeople.
  5. Когда 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())
}

Пользователи все еще могут попытаться ввести текст, как показано здесь:

Но форматер требует использования номера.

Первый пост здесь, прошу простить все ошибки. Я боролся с этим вопросом в моем текущем проекте. Многие ответы работают хорошо, но только для определенных проблем, и в моем случае ни один из них не отвечал всем требованиям.

В частности, мне понадобились:

  1. Пользовательский ввод только числовых значений, включая отрицательные числа, в несколько текстовых полей.
  2. Привязка этого ввода к переменной типа 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

  1. TextField разрешает только числовое значение
  2. Должен принимать только одну запятую ("".")
  3. Ограничить десятичную точку до 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
}
}

.keyboardType(.numberPad).keyboardType(.emailAddress)

Другие вопросы по тегам