SwiftUI 验证文本字段中的输入

Posted

技术标签:

【中文标题】SwiftUI 验证文本字段中的输入【英文标题】:SwiftUI validate input in textfields 【发布时间】:2020-04-07 05:04:17 【问题描述】:

我正在尝试通过使用正则表达式删除某些字符来验证 TextField 中的用户输入。不幸的是,我遇到了 text var 递归调用自身的 didSet 方法的问题。

import SwiftUI
import Combine

class TextValidator: ObservableObject 

    @Published var text = "" 
        didSet 
            print("didSet")
            text = text.replacingOccurrences(
                of: "\\W", with: "", options: .regularExpression
            ) // `\W` is an escape sequence that matches non-word characters.
        
    




struct ContentView: View 

    @ObservedObject var textValidator = TextValidator()

    var body: some View 
        TextField("Type Here", text: $textValidator.text)
            .padding(.horizontal, 20.0)
            .textFieldStyle(RoundedBorderTextFieldStyle())

    

在swift docs(参见 AudioChannel 结构)中,Apple 提供了一个示例,其中在其自己的 didSet 方法中重新分配了一个属性,并明确指出这不会导致再次调用 didSet 方法。我在操场上做了一些测试并确认了这种行为。但是,当我使用 ObservableObjectPublished 变量时,情况似乎有所不同。

如何防止 didSet 方法递归调用自身?

我尝试了post 中的示例,但没有一个有效。从那时起,Apple 可能已经改变了一些事情,所以这篇文章不是那个重复的。

此外,在遇到无效字符时在 didSet 方法中将文本设置回 oldValue 意味着如果用户粘贴文本,则整个文本将被删除,而不是仅删除无效字符。所以这个选项不起作用。

【问题讨论】:

【参考方案1】:

尝试在 TextField onRecive 方法中验证您想要的内容,如下所示:

class TextValidator: ObservableObject 

    @Published var text = ""



struct ContentView: View 

    @ObservedObject var textValidator = TextValidator()
    var body: some View 
        TextField("Type Here", text: $textValidator.text)
            .padding(.horizontal, 20.0)
            .textFieldStyle(RoundedBorderTextFieldStyle())
            .onReceive(Just(textValidator.text))  newValue in
                let value = newValue.replacingOccurrences(
                    of: "\\W", with: "", options: .regularExpression)
                if value != newValue 
                    self.textValidator.text = value
                
                print(newValue)
        
    

【讨论】:

哇!我从来不知道修饰符的存在! 哇!这是我一直在寻找的解决方案。现在需要查看组合,因为可能有很多宝石等待被发现......非常感谢【参考方案2】:

从 SwiftUI 2 开始,您可以使用 onChange 方法检查输入并在那里进行任何验证或更改:

TextField("", value: $text)
    .onChange(of: text)  [text] newValue in
         // do any validation or alteration here.
         // 'text' is the old value, 'newValue' is the new one.
    

【讨论】:

【参考方案3】:

这是使用代理绑定的可能方法,它仍然允许分离视图和视图模型逻辑

class TextValidator: ObservableObject 

    @Published var text = ""

    func validate(_ value: String) -> String 
        value.replacingOccurrences(
                of: "\\W", with: "", options: .regularExpression
            )
    



struct ContentView: View 

    @ObservedObject var textValidator = TextValidator()

    var body: some View 
        let validatingText = Binding<String>(
                get:  self.textValidator.text ,
                set:  self.textValidator.text = self.textValidator.validate($0) 
                )
        return TextField("Type Here", text: validatingText)
            .padding(.horizontal, 20.0)
            .textFieldStyle(RoundedBorderTextFieldStyle())

    

【讨论】:

谢谢,这行得通。你为什么递归调用我的代码版本中的 didSet 方法?另外,你能解释一下你的代码吗?【参考方案4】:

由于在设置值时总是调用didSetwillSet,并且objectWillChange 会触发对TextField 的更新(这会再次触发didSet),因此当基础值无条件更新时会创建一个循环在didSet.

更新基础值有条件地中断循环。 例如:

import Combine

class TextValidator: ObservableObject 
    @Published var text = "" 
        didSet 
            if oldValue == text || text == acceptableValue(oldValue)  
                return
            
            text = acceptableValue(text)
        
    
    
    var acceptableValue: (String) -> String =  $0 


import SwiftUI

struct TestingValidation: View 
    @StateObject var textValidator: TextValidator = 
        let o = TextValidator()
        o.acceptableValue =  $0.replacingOccurrences(
            of: "\\W", with: "", options: .regularExpression) 
        return o
    ()
    
    @StateObject var textValidator2: TextValidator = 
        let o = TextValidator()
        o.acceptableValue =  $0.replacingOccurrences(
                of: "\\D", with: "", options: .regularExpression) 
        return o
    ()
    
   
    var body: some View 
            VStack 
                Text("Word characters only")
                TextField("Type here", text: $textValidator.text)
                
                Text("Digits only")
                TextField("Type here", text: $textValidator2.text)
            
            .padding(.horizontal, 20.0)
            .textFieldStyle(RoundedBorderTextFieldStyle())
            .disableAutocorrection(true)
            .autocapitalization(.none)
    

【讨论】:

【参考方案5】:

2021 | SwiftUI 2

自定义扩展用法:

TextField("New Branch name", text: $model.newNameUnified)
    .ignoreSymbols( symbols: [" ", "\n"], string: $model.newNameUnified )

扩展名:

@available(OSX 11.0, *)
public extension TextField 
    func ignoreSymbols(symbols: [Character], string: Binding<String>) -> some View 
         self.modifier( IgnoreSymbols(symbols: symbols, string: string)  )
    


@available(OSX 11.0, *)
public struct IgnoreSymbols: ViewModifier 
    var symbols: [Character]
    var string: Binding<String>
    
    public func body (content: Content) -> some View
    
        content.onChange(of: string.wrappedValue)  value in
            var newValue = value
        
            for symbol in symbols 
                newValue = newValue.replace(of: "\(symbol)", to: "")
            
        
            if value != newValue 
                string.wrappedValue = newValue
            
        
    

【讨论】:

【参考方案6】:

这是我的想法:

struct ValidatableTextField: View 
    let placeholder: String
    @State private var text = ""
    var validation: (String) -> Bool
  
    @Binding private var sourceText: String
  
    init(_ placeholder: String, text: Binding<String>, validation: @escaping (String) -> Bool) 
        self.placeholder = placeholder
        self.validation = validation
        self._sourceText = text
    
        self.text = text.wrappedValue
    
  
    var body: some View 
        TextField(placeholder, text: $text)
            .onChange(of: text)  newValue in
                if validation(newValue) 
                    self.sourceText = newValue
                 else 
                  self.text = sourceText
                
            
    

用法:

ValidatableTextField("Placeholder", text: $text, validation:  !$0.contains("%") )

注意:此代码不能专门解决您的问题,但会展示如何处理一般验证。

更改正文以解决您的问题:

TextField(placeholder, text: $text)
    .onChange(of: text)  newValue in
        let value = newValue.replacingOccurrences(of: "\\W", with: "", options: .regularExpression)
        if value != newValue 
            self.sourceText = newValue
            self.text = sourceText
        
    

【讨论】:

以上是关于SwiftUI 验证文本字段中的输入的主要内容,如果未能解决你的问题,请参考以下文章

我的搜索栏中的文本字段不允许输入文本(swiftUI)

ForEach 通过多个 TextField 来验证 SwiftUI 中是不是为空

在 SwiftUI 中的 forEach 期间将文本字段输入添加到数组

从文本字段输入在现有表中创建新行。 SwiftUI(故事板)

SwiftUI - 如何修改数组中的文本字段

如何从 webkit swiftUI 中的文本字段中搜索自定义文本