SwiftUI TextField Avançado — Formatação e Validação
TextField é provavelmente o componente de entrada de texto mais usado por desenvolvedores em aplicativos SwiftUI. Como um wrapper SwiftUI para UITextField (NSTextField), a Apple fornece aos desenvolvedores vários construtores e modificadores para melhorar sua usabilidade e personalização. No entanto, o SwiftUI também protege muitas interfaces e recursos avançados em seu encapsulamento, aumentando a complexidade para os desenvolvedores implementarem certas necessidades específicas. Este artigo é uma das séries de artigos 【SwiftUI Advanced】. Neste artigo, apresentarei como implementar os seguintes recursos no TextField:
- Proteger caracteres inválidos
- Determine se o conteúdo de entrada atende a condições específicas
- Formato em tempo real e exibir o texto de entrada

Por que não envolver você mesmo uma nova implementação
Para muitos desenvolvedores que mudam de UIKit para SwiftUI, é natural pensar em agrupar sua própria implementação por meio de UIViewRepresentable quando a API oficial do SwiftUI não pode atender a certos requisitos (consulte Usando visualizações UIKit no SwiftUI para obter mais informações). Nos primeiros dias do SwiftUI, essa era realmente uma abordagem muito eficaz. No entanto, à medida que o SwiftUI amadurece gradualmente, a Apple forneceu um grande número de recursos exclusivos para a API do SwiftUI. Não vale a pena abrir mão de usar a solução oficial SwiftUI apenas por algumas exigências.
Portanto, nos últimos meses, abandonei gradualmente a ideia de implementar certos requisitos por meio de auto-encapsulamento ou usando outras bibliotecas de extensão de terceiros. Ao adicionar novos recursos ao SwiftUI, tento seguir os seguintes princípios tanto quanto possível:
- Priorize se uma solução pode ser encontrada nos métodos nativos do SwiftUI
- Se métodos não nativos forem realmente necessários, tente adotar uma implementação não destrutiva, e a nova função não pode sacrificar a função original (ela precisa ser compatível com o método modificador SwiftUI oficial)
Como implementar a exibição formatada no TextField
Métodos de formatação existentes
No SwiftUI 3.0, TextField adicionou dois métodos de construção que usam formatadores novos e antigos. Os desenvolvedores podem usar diretamente tipos de dados não String (como números inteiros, números de ponto flutuante, datas etc.) e formatar o conteúdo inserido por meio do Formatter. Por exemplo:
struct FormatterDemo:View{
@State var number = 100
var body: some View{
Form{
TextField("inputNumber",value:$number,format: .number)
}
}
}
Possíveis soluções de formatação
- Ative o formatador integrado no TextField durante o processo de entrada para permitir que ele formate o conteúdo quando o texto for alterado.
- Chame nossa própria implementação do método Format para formatar o conteúdo em tempo real quando o texto for alterado.
TextField("inputNumber",value:$number,format: .number)
.introspectTextField{ td in
td.delegate = nil
}
A segunda abordagem é não usar nenhuma “magia negra” e usar apenas o método nativo SwiftUI para formatar o texto quando a entrada for alterada. A solução dois deste artigo é uma implementação específica dessa abordagem.
Como bloquear caracteres inválidos em TextField
Métodos de bloqueio de caracteres existentes
No SwiftUI, as restrições de entrada podem ser alcançadas até certo ponto, definindo tipos de teclado específicos. Por exemplo, o código a seguir permite apenas que os usuários insiram números:
TextField("inputNumber",value:$number,format: .number)
.keyboardType(.numberPad)
- Suporta apenas certos tipos de dispositivos
- Suporte limitado para tipos de teclado
Além disso, devido ao suporte limitado para tipos de teclado, geralmente é difícil usá-lo em muitos cenários de aplicativos. O exemplo mais típico é que numberPad não suporta números negativos, o que significa que só pode ser usado para números inteiros positivos. Alguns desenvolvedores podem resolver isso personalizando o teclado ou adicionando inputAccessoryView, mas para outros desenvolvedores que não têm habilidade ou energia, também é uma boa solução bloquear diretamente os caracteres inválidos inseridos.
Possíveis soluções de caracteres bloqueados:
- Use o
textField
método deUITextFieldDelegate
- Nas visualizações do SwiftUI, use
onChange
para verificar e modificar a entrada quando ocorrerem alterações
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
// Check if the string meets the requirements
if meetsRequirements { return true } // Add the new character to the input box
else { return false}
}
Para a segunda abordagem, apoiamos a poupança seletiva, mas ela também tem limitações. Devido ao método especial de empacotamento usado pelo construtor Formatter do TextField, não podemos obter o conteúdo da caixa de entrada quando o valor de ligação não é a ( String
como inteiros, números de ponto flutuante, datas, etc.). Portanto, usando essa abordagem, podemos usar apenas strings como o tipo de ligação e não podemos aproveitar a conveniência trazida pelo novo método de construção do SwiftUI. O Esquema 2 adota esta abordagem.
Como verificar se o conteúdo em TextField atende às condições especificadas em SwiftUI
Comparado aos dois objetivos acima, é muito fácil verificar se o conteúdo em TextField atende às condições especificadas em SwiftUI. Por exemplo:
TextField("inputNumber", value: $number, format: .number)
.foregroundColor(number < 100 ? .red : .primary)
Claro, também podemos continuar a solução acima verificando o texto no textfield
método do delegado. No entanto, essa abordagem não é muito versátil para tipos não string (que requerem conversão).
Outras questões a considerar
Antes de usar a abordagem acima para a programação real, precisamos considerar algumas outras questões:
Localização
O código de demonstração fornecido neste artigo (https://github.com/fatbobman/TextFieldFomatAndValidateDemo) implementa processamento em tempo real para dois tipos de dados: Int
e Double
. Embora esses dois tipos sejam principalmente numéricos, questões de localização ainda precisam ser consideradas ao processá-los.
Para diferentes regiões, o ponto decimal e o separador de agrupamento para números podem ser diferentes, por exemplo:
1,000,000.012 // Most regions
1 000 000,012 // fr
Se você precisar lidar com dados de data ou outro formato personalizado, também é melhor fornecer um procedimento de processamento para caracteres localizados no código.
Formatador
Atualmente, o TextField do SwiftUI fornece métodos de construção correspondentes para Formatters novos e antigos. Prefiro usar a nova API do Formatter, que é uma implementação nativa do Swift da antiga API do Formatter, fornecendo um método de declaração mais conveniente e seguro. Para obter mais informações sobre o novo formatador, leia a API do novo formatador da Apple: comparação entre o antigo e o novo e como personalizar .
No entanto, o TextField ainda tem alguns problemas com o suporte ao novo Formatador, portanto, atenção especial deve ser dada ao escrever o código. Por exemplo,
@State var number = 100
TextField("inputNumber", value: $number, format: .number)
Se pretendemos apenas atingir o objetivo originalmente definido neste artigo, não é tão complicado. No entanto, a implementação deve fornecer métodos de chamada convenientes e minimizar a poluição do código original.
Por exemplo, o código a seguir mostra os métodos de chamada para a Solução 1 e a Solução 2.
// Solution 1
let intDelegate = ValidationDelegate(type: .int, maxLength: 6)
TextField("0...1000", value: $intValue, format: .number)
.addTextFieldDelegate(delegate: intDelegate)
.numberValidator(value: intValue) { $0 < 0 || $0 > 1000 }
// Solution 2
@StateObject var intStore = NumberStore(text: "",
type: .int,
maxLength: 5,
allowNagative: true,
formatter: IntegerFormatStyle<Int>())
TextField("-1000...1000", text: $intStore.text)
.formatAndValidate(intStore) { $0 < -1000 || $0 > 1000 }
Solução 1
Você pode baixar o código de demonstração deste artigo no Github. Apenas parte do código é explicada no artigo, por favor, consulte o código-fonte para a implementação completa.
O plano um usa o novo método construtor Formatter de TextField:
public init<F>(_ titleKey: LocalizedStringKey, value: Binding<F.FormatInput>, format: F, prompt: Text? = nil) where F : ParseableFormatStyle, F.FormatOutput == String
Bloquear caracteres inválidos:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let text = textField.text ?? ""
return validator(text: text, replacementString: string)
}
private func validator(text: String, replacementString string: String) -> Bool {
// Check valid characters
guard string.allSatisfy({ characters.contains($0) }) else { return false }
let totalText = text + string
// Check decimal point
if type == .double, text.contains(decimalSeparator), string.contains(decimalSeparator) {
return false
}
// Check negative sign
let minusCount = totalText.components(separatedBy: minusCharacter).count - 1
if minusCount > 1 {
return false
}
if minusCount == 1, !totalText.hasPrefix("-") {
return false
}
// Check length
guard totalText.count < maxLength + minusCount else {
return false
}
return true
}
Adicionando extensões de visualização
extension View {
// Adjust text color based on whether the specified condition is met
func numberValidator<T: Numeric>(value: T, errorCondition: (T) -> Bool) -> some View {
foregroundColor(errorCondition(value) ? .red : .primary)
}
// Replace delegate
func addTextFieldDelegate(delegate: UITextFieldDelegate) -> some View {
introspectTextField { td in
td.delegate = delegate
}
}
}
A solução 2 usa métodos nativos do SwiftUI para atingir o mesmo objetivo. Como não é possível usar o Formatador ou os recursos de texto bruto incorporados ao TextField, a implementação é um pouco mais complexa do que a Solução 1. Além disso, para validar os caracteres inseridos em tempo real, apenas tipos de string podem ser usados como ligação tipo de TextField, o que torna a chamada um pouco mais complexa do que a Solução 1 (que pode ser ainda mais simplificada agrupando-a novamente).
Para salvar alguns dados temporários, precisamos criar uma classe que esteja em conformidade com ObservableObject para gerenciar os dados uniformemente.
class NumberStore<T: Numeric, F: ParseableFormatStyle>: ObservableObject where F.FormatOutput == String, F.FormatInput == T {
@Published var text: String
let type: ValidationType
let maxLength: Int
let allowNagative: Bool
private var backupText: String
var error: Bool = false
private let locale: Locale
let formatter: F
init(text: String = "",
type: ValidationType,
maxLength: Int = 18,
allowNagative: Bool = false,
formatter: F,
locale: Locale = .current)
{
self.text = text
self.type = type
self.allowNagative = allowNagative
self.formatter = formatter
self.locale = locale
backupText = text
self.maxLength = maxLength == .max ? .max - 1 : maxLength
}
// Return the validated number.
func getValue() -> T? {
// Special handling (empty, minus sign only, decimal point as the first character for floating point numbers).
if text.isEmpty || text == minusCharacter || (type == .double && text == decimalSeparator) {
backup()
return nil
}
// Check if the characters are valid by removing the grouping separator from the string.
let pureText = text.replacingOccurrences(of: groupingSeparator, with: "")
guard pureText.allSatisfy({ characters.contains($0) }) else {
restore()
return nil
}
// Handle multiple decimal points.
if type == .double {
if text.components(separatedBy: decimalSeparator).count > 2 {
restore()
return nil
}
}
// Handle multiple minus signs.
if minusCount > 1 {
restore()
return nil
}
// The minus sign must be the first character.
if minusCount == 1, !text.hasPrefix("-") {
restore()
return nil
}
// Check the length.
guard text.count < maxLength + minusCount else {
restore()
return nil
}
// Convert the text to a number, then convert it to a string (to ensure the text format is correct).
if let value = try? formatter.parseStrategy.parse(text) {
let hasDecimalCharacter = text.contains(decimalSeparator)
text = formatter.format(value)
// Protect the last decimal point (otherwise, the converted text might not contain the decimal point).
if hasDecimalCharacter, !text.contains(decimalSeparator) {
text.append(decimalSeparator)
}
backup()
return value
} else {
restore()
return nil
}
}
Além disso, precisamos considerar os casos em que o primeiro caractere é `` e o último caractere é um ponto decimal, pois parseStrategy pode perder essa informação durante a conversão. Precisamos reproduzir esses caracteres no resultado final da conversão.
Exibir extensão
extension View {
@ViewBuilder
func formatAndValidate<T: Numeric, F: ParseableFormatStyle>(_ numberStore: NumberStore<T, F>, errorCondition: @escaping (T) -> Bool) -> some View {
onChange(of: numberStore.text) { text in
if let value = numberStore.getValue(),!errorCondition(value) {
numberStore.error = false // Save validation status through NumberStore
} else if text.isEmpty || text == numberStore.minusCharacter {
numberStore.error = false
} else { numberStore.error = true }
}
.foregroundColor(numberStore.error ? .red : .primary)
.disableAutocorrection(true)
.autocapitalization(.none)
.onSubmit { // Handle the case when there is only one decimal point
if numberStore.text.count > 1 && numberStore.text.suffix(1) == numberStore.decimalSeparator {
numberStore.text.removeLast()
}
}
}
}
Como onChange
só é chamado após a alteração do texto, a Solução 2 fará com que a exibição seja atualizada duas vezes. No entanto, considerando o cenário do aplicativo de entrada de texto, a perda de desempenho pode ser ignorada (se o link entre o valor e a string for vinculado posteriormente usando wrappers de propriedade, poderá aumentar ainda mais o número de atualizações de exibição).
Você pode baixar o código de demonstração deste artigo no Github .
Comparação de Duas Soluções
- Eficiência
- Tipos de dados compatíveis
- Suporte de valor opcional
A solução dois permite não fornecer um valor inicial e suporta valores opcionais.
Além disso, na solução um, se todos os caracteres forem apagados, a variável vinculada ainda terá um valor (comportamento original da API), o que pode causar confusão para os usuários durante a entrada.
- Sustentabilidade (compatibilidade com versões anteriores do SwiftUI)
- Compatibilidade com outros métodos de decoração
Conclusão
Todo desenvolvedor espera fornecer aos usuários um ambiente de interação eficiente e elegante. Este artigo envolve apenas parte do conteúdo do TextField. Em outras seções de [ SwiftUI TextField Advanced ], exploraremos mais técnicas e ideias para permitir que os desenvolvedores criem diferentes experiências de entrada de texto no SwiftUI.

Espero que este artigo possa ser útil para você. Você também pode se comunicar comigo através do Twitter , do canal Discord ou do quadro de mensagens do meu blog .