Swift - 如何在每个单独的 SMS Otp UITextfield 中放置多个字符

Posted

技术标签:

【中文标题】Swift - 如何在每个单独的 SMS Otp UITextfield 中放置多个字符【英文标题】:Swift -How to put multiple characters in each individual SMS Otp UITextfield 【发布时间】:2019-03-11 12:43:31 【问题描述】:

我正在尝试以这种格式输入用户酋长国ID

为此,我尝试关注

https://***.com/a/54601324/428240

如果我有一个字符,它工作正常,但如果你检查图像,我希望第一个文本字段显示 3 个字符,第二个显示 4,第三个显示 7,第四个文本字段显示 1 个字符。而且如果我尝试从文本字段中删除字符,它不应该全部删除,而是只一个一个删除,然后在文本字段中有注释时移动到另一个。

谁能帮我解决这个问题。在此先感谢

protocol EmiratesIdTextFieldDelegate: class 
    func textFieldDidDelete()


import UIKit

class EmiratesIdTextField: UITextField 

weak var emiratesIdTextFieldDelegate: EmiratesIdTextFieldDelegate? // make sure to declare this as weak to prevent a memory leak/retain cycle

override func deleteBackward() 
    super.deleteBackward()
    emiratesIdTextFieldDelegate?.textFieldDidDelete()


// when a char is inside the textField this keeps the cursor to the right of it. If the user can get on the left side of the char and press the backspace the current char won't get deleted
override func closestPosition(to point: CGPoint) -> UITextPosition? 
    let beginning = self.beginningOfDocument
    let end = self.position(from: beginning, offset: self.text?.count ?? 0)
    return end




class PinViewController: UIViewController,EmiratesIdTextFieldDelegate,UITextFieldDelegate 
@IBOutlet weak var textField1: EmiratesIdTextField!
@IBOutlet weak var textField2: EmiratesIdTextField!
@IBOutlet weak var textField3: EmiratesIdTextField!
@IBOutlet weak var textField4: EmiratesIdTextField!

var activeTextField = UITextField()

override func viewDidLoad() 
    super.viewDidLoad()

    textField1.delegate = self
    textField2.delegate = self
    textField3.delegate = self
    textField4.delegate = self

    textField1.emiratesIdTextFieldDelegate = self
    textField2.emiratesIdTextFieldDelegate = self
    textField3.emiratesIdTextFieldDelegate = self
    textField4.emiratesIdTextFieldDelegate = self

//  configureAnchors()

    textField1.becomeFirstResponder()


func textFieldDidBeginEditing(_ textField: UITextField) 

    activeTextField = textField


func textFieldDidDelete() 

    if activeTextField == textField1 
        print("backButton was pressed in otpTextField1")
        // do nothing
    

    if activeTextField == textField2 
        print("backButton was pressed in otpTextField2")
        textField2.isEnabled = false
        textField1.isEnabled = true
        textField1.becomeFirstResponder()
        textField1.text = ""
    

    if activeTextField == textField3 
        print("backButton was pressed in otpTextField3")
        textField3.isEnabled = false
        textField2.isEnabled = true
        textField2.becomeFirstResponder()
        textField2.text = ""
    

    if activeTextField == textField4 
        print("backButton was pressed in otpTextField4")
        textField4.isEnabled = false
        textField3.isEnabled = true
        textField3.becomeFirstResponder()
        textField3.text = ""
    


func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool 

    let text = textField.text

    if let text = text 

        // 10. when the user enters something in the first textField it will automatically adjust to the next textField and in the process do some disabling and enabling. This will proceed until the last textField
        if (text.count < 1) && (string.count > 0) 

            if textField == textField1 
                textField1.isEnabled = false
                textField2.isEnabled = true
                textField2.becomeFirstResponder()
            

            if textField == textField2 
                textField2.isEnabled = false
                textField3.isEnabled = true
                textField3.becomeFirstResponder()
            

            if textField == textField3 
                textField3.isEnabled = false
                textField4.isEnabled = true
                textField4.becomeFirstResponder()
            

            if textField == textField4 
                // do nothing
            

            textField.text = string
            return false

         // 11. if the user gets to the last textField and presses the back button everything above will get reversed
        else if (text.count >= 1) && (string.count == 0) 

            if textField == textField2 
                textField2.isEnabled = false
                textField2.isEnabled = true
                textField1.becomeFirstResponder()
                textField1.text = ""
            

            if textField == textField3 
                textField3.isEnabled = false
                textField2.isEnabled = true
                textField2.becomeFirstResponder()
                textField2.text = ""
            

            if textField == textField4 
                textField4.isEnabled = false
                textField3.isEnabled = true
                textField3.becomeFirstResponder()
                textField3.text = ""
            

            if textField == textField1 
                // do nothing
            

            textField.text = ""
            return false

         // 12. after pressing the backButton and moving forward again you will have to do what's in step 10 all over again
        else if text.count >= 1 

            if textField == textField1 
                textField1.isEnabled = false
                textField2.isEnabled = true
                textField2.becomeFirstResponder()
            

            if textField == textField2 
                textField2.isEnabled = false
                textField3.isEnabled = true
                textField3.becomeFirstResponder()
            

            if textField == textField3 
                textField3.isEnabled = false
                textField4.isEnabled = true
                textField4.becomeFirstResponder()
            

            if textField == textField4 
                // do nothing
            

            textField.text = string
            return false
        
    
    return true


【问题讨论】:

如果您添加一些代码会很棒。 添加了代码。基本上我没有添加,因为它与问题中提到的线程相同, 投了反对票,因为您的问题没有指出您在哪里遇到问题或您试图让这些组件在哪里工作。你从另一个答案中提取了这个问题的代码,就是这样。 【参考方案1】:

这类似于my other answer。您可以将其复制并粘贴到一个文件中并运行它以查看它是如何工作的。

这个是不同的,因为 OP 想知道阿联酋航空如何使用每个文本字段中的多个数字来使用他们的文本字段。我不知道他们的工作原理,但这就是 UberEats 的短信文本字段的工作方式,所以我将它们结合起来,允许每个文本字段中有多个数字。您不能只是随机按下文本字段并选择它。使用它你只能向前和向后移动。用户体验是主观的,但如果 Uber 使用它,用户体验必须是有效的。我说它是相似的,因为它们也有一个灰色框覆盖 textField,所以我不确定它背后发生了什么。这是我能得到的最接近的。

首先你必须继承 UITextField using this answer 来检测退格按钮何时被按下。当按下后退按钮时,第 11 步中的每个 textField 都有相关的逻辑来确定每个 textField 中应该包含哪些文本。

其次,一旦字符位于 textField using this answer 内,您将不得不阻止用户选择光标的左侧。您从第一步开始覆盖同一子类中的方法。

第三,你需要检测哪个 textField 当前处于活动状态using this answer

第四次向每个 textField 添加一个 addTarget 方法,以便在用户键入时进行监控。我将它添加到每个 textField 中,您将在 viewDidLoad 底部的第 8 步中看到它们。关注this answer 看看它是如何工作的。

我正在以编程方式完成所有操作,因此您可以将整个代码复制并粘贴到项目中并运行它

首先创建一个 UITextField 的子类并将其命名为 MyTextField(它在文件的顶部)。

第二个在带有 OTP 文本字段的类中,将类设置为使用 UITextFieldDelegate 和 MyTextFieldDelegate,然后创建一个类属性并将其命名为 activeTextField。当 textFieldDidBeginEditing 中的任何 textField 变为活动状态时,您将 activeTextField 设置为该值。在 viewDidLoad 中,将所有 textFields 设置为使用两个委托。 addTargets 方法也在那里。您可以将它们放在每个textField closure 中伴随代表方法,以获得更简洁的代码。如果您这样做,请将每个文本字段更改为以 lazy var 而不是 let 开头。

确保第一个 otpTextField 已启用,第二个、第三个和第四个 otpTextField 最初都已禁用

所有的内容都在 cmets 上面的代码行 1-12 中进行了解释

import UIKit

protocol MyTextFieldDelegate: class 
    func textFieldDidDelete()


// 1. subclass UITextField and create protocol for it to know when the backButton is pressed
class MyTextField: UITextField 

    weak var myDelegate: MyTextFieldDelegate? // make sure to declare this as weak to prevent a memory leak/retain cycle

    override func deleteBackward() 
        super.deleteBackward()
        myDelegate?.textFieldDidDelete()
    

    // when a char is inside the textField this keeps the cursor to the right of it. If the user can get on the left side of the char and press the backspace the current char won't get deleted
    override func closestPosition(to point: CGPoint) -> UITextPosition? 
        let beginning = self.beginningOfDocument
        let end = self.position(from: beginning, offset: self.text?.count ?? 0)
        return end
    


// 2. set the class to use BOTH Delegates
class ViewController: UIViewController, UITextFieldDelegate, MyTextFieldDelegate 

    let staticLabel: UILabel = 
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.font = UIFont.systemFont(ofSize: 17)
        label.text = "Enter the SMS code sent to your phone"
        return label
    ()

    // 3. make each textField of type MYTextField
    let otpTextField1: MyTextField = 
        let textField = MyTextField()
        textField.translatesAutoresizingMaskIntoConstraints = false
        textField.font = UIFont.systemFont(ofSize: 18)
        textField.autocorrectionType = .no
        textField.keyboardType = .numberPad
        textField.textAlignment = .center
        // **important this is initially ENABLED
        return textField
    ()

    let otpTextField2: MyTextField = 
        let textField = MyTextField()
        textField.translatesAutoresizingMaskIntoConstraints = false
        textField.font = UIFont.systemFont(ofSize: 18)
        textField.autocorrectionType = .no
        textField.keyboardType = .numberPad
        textField.textAlignment = .center
        textField.isEnabled = false // **important this is initially DISABLED
        return textField
    ()

    let otpTextField3: MyTextField = 
        let textField = MyTextField()
        textField.translatesAutoresizingMaskIntoConstraints = false
        textField.font = UIFont.systemFont(ofSize: 18)
        textField.autocorrectionType = .no
        textField.keyboardType = .numberPad
        textField.textAlignment = .center
        textField.isEnabled = false // **important this is initially DISABLED
        return textField
    ()

    let otpTextField4: MyTextField = 
        let textField = MyTextField()
        textField.translatesAutoresizingMaskIntoConstraints = false
        textField.font = UIFont.systemFont(ofSize: 18)
        textField.autocorrectionType = .no
        textField.keyboardType = .numberPad
        textField.textAlignment = .center
        textField.isEnabled = false // **important this is initially DISABLED
        return textField
    ()

    // 4. create this property to know which textField is active. Set it in step 8 and use it in step 9
    var activeTextField = UITextField()

    override func viewDidLoad() 
        super.viewDidLoad()
        view.backgroundColor = .white

        // 5. set the regular UItextField delegate to each textField
        otpTextField1.delegate = self
        otpTextField2.delegate = self
        otpTextField3.delegate = self
        otpTextField4.delegate = self

        // 6. set the subClassed textField delegate to each textField
        otpTextField1.myDelegate = self
        otpTextField2.myDelegate = self
        otpTextField3.myDelegate = self
        otpTextField4.myDelegate = self

        configureAnchors()

        // 7. once the screen appears show the keyboard
        otpTextField1.becomeFirstResponder()

        // 8. add this method to each textField's target action so it can be monitored while the user is typing
        otpTextField1.addTarget(self, action: #selector(monitorTextFieldWhileTyping(_ :)), for: .editingChanged)
        otpTextField2.addTarget(self, action: #selector(monitorTextFieldWhileTyping(_ :)), for: .editingChanged)
        otpTextField3.addTarget(self, action: #selector(monitorTextFieldWhileTyping(_ :)), for: .editingChanged)
        otpTextField4.addTarget(self, action: #selector(monitorTextFieldWhileTyping(_ :)), for: .editingChanged)
    

    override func viewDidLayoutSubviews() 
        super.viewDidLayoutSubviews()

        addBottomLayerTo(textField: otpTextField1)
        addBottomLayerTo(textField: otpTextField2)
        addBottomLayerTo(textField: otpTextField3)
        addBottomLayerTo(textField: otpTextField4)
    

    // 9. when a textField is active set the activeTextField property to that textField
    func textFieldDidBeginEditing(_ textField: UITextField) 

        activeTextField = textField
    

    // 10. when the backButton is pressed, the MyTextField delegate will get called. The activeTextField will let you know which textField the backButton was pressed in. Depending on the textField certain textFields will become enabled and disabled.
    func textFieldDidDelete() 

        if activeTextField == otpTextField1 
            print("backButton was pressed in otpTextField1")
            // do nothing
        

        if activeTextField == otpTextField2 
            print("backButton was pressed in otpTextField2")
            otpTextField2.isEnabled = false
            otpTextField1.isEnabled = true
            otpTextField1.becomeFirstResponder()
        

        if activeTextField == otpTextField3 
            print("backButton was pressed in otpTextField3")
            otpTextField3.isEnabled = false
            otpTextField2.isEnabled = true
            otpTextField2.becomeFirstResponder()
        

        if activeTextField == otpTextField4 
            print("backButton was pressed in otpTextField4")
            otpTextField4.isEnabled = false
            otpTextField3.isEnabled = true
            otpTextField3.becomeFirstResponder()
        
    

    // 11. as the user types it will check which textField you are in and then once the text.count is what you want for that textField it will jump to the next textField
    @objc func monitorTextFieldWhileTyping(_ textField: UITextField) 

        if let text = textField.text 

            if text.count >= 1 

                // otpTextField1
                if textField == otpTextField1 

                    if let textInOtpTextField1 = otpTextField1.text 

                        if textInOtpTextField1.count == 3 
                            otpTextField1.isEnabled = false
                            otpTextField2.isEnabled = true
                            otpTextField2.becomeFirstResponder()
                        

                        // when the user presses the back button in textInOtpTextField2, now that they're back in textInOtpTextField1 if the conditional statement below is met this will execute
                        if textInOtpTextField1.count > 3 

                            let firstThreeCharsInTextField1 = textInOtpTextField1.prefix(3)

                            let convertFirstThreeToString = String(firstThreeCharsInTextField1)
                            DispatchQueue.main.async  [weak self] in
                                self?.otpTextField1.text = convertFirstThreeToString
                            

                            otpTextField1.isEnabled = false
                            otpTextField2.isEnabled = true
                            otpTextField2.becomeFirstResponder()

                            let convertLastCharToString = String(textInOtpTextField1.last!)
                            otpTextField2.text = convertLastCharToString
                        
                    
                

                // otpTextField2
                if textField == otpTextField2 

                    if let textInOtpTextField2 = otpTextField2.text 

                        if textInOtpTextField2.count == 4 

                            otpTextField2.isEnabled = false
                            otpTextField3.isEnabled = true
                            otpTextField3.becomeFirstResponder()
                        

                        // when the user presses the back button in textInOtpTextField3, now that they're back in textInOtpTextField2 if the conditional statement below is met this will execute
                        if textInOtpTextField2.count > 4 

                            let firstFourCharsInTextField2 = textInOtpTextField2.prefix(4)

                            let convertFirstFourToString = String(firstFourCharsInTextField2)
                            DispatchQueue.main.async  [weak self] in
                                self?.otpTextField2.text = convertFirstFourToString
                            

                            otpTextField2.isEnabled = false
                            otpTextField3.isEnabled = true
                            otpTextField3.becomeFirstResponder()

                            let convertLastCharToString = String(textInOtpTextField2.last!)
                            otpTextField3.text = convertLastCharToString
                        
                    
                

                // otpTextField3
                if textField == otpTextField3 

                    if let textInOtpTextField3 = otpTextField3.text 

                        if textInOtpTextField3.count == 7 
                            otpTextField3.isEnabled = false
                            otpTextField4.isEnabled = true
                            otpTextField4.becomeFirstResponder()
                        

                        // when the user presses the back button in textInOtpTextField4, now that they're back in textInOtpTextField4 if the conditional statement below is met this will execute
                        if textInOtpTextField3.count > 7 

                            let firstSevenCharsInTextField3 = textInOtpTextField3.prefix(7)

                            let convertFirstSevenToString = String(firstSevenCharsInTextField3)
                            DispatchQueue.main.async  [weak self] in
                                self?.otpTextField3.text = convertFirstSevenToString
                            

                            otpTextField3.isEnabled = false
                            otpTextField4.isEnabled = true
                            otpTextField4.becomeFirstResponder()

                            let convertLastCharToString = String(textInOtpTextField3.last!)
                            otpTextField4.text = convertLastCharToString
                        
                    
                

                if textField == otpTextField4 
                    // do nothing
                

                textField.text = text
            
        
    

    // 12. Use this delegate method to limit the text in otpTextField4 so that it can only take 1 character
    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool 

        if textField == otpTextField4 

            textField.text = string
            return false
        
        return true
    


extension ViewController 

    // this adds a lightGray line at the bottom of the textField
    func addBottomLayerTo(textField: UITextField) 
        let layer = CALayer()
        layer.backgroundColor = UIColor.lightGray.cgColor
        layer.frame = CGRect(x: 0, y: textField.frame.height - 2, width: textField.frame.width, height: 2)
        textField.layer.addSublayer(layer)
    

    func configureAnchors() 

        view.addSubview(staticLabel)

        view.addSubview(otpTextField1)
        view.addSubview(otpTextField2)
        view.addSubview(otpTextField3)
        view.addSubview(otpTextField4)

        let width = view.frame.width / 5

        staticLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 15).isActive = true
        staticLabel.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 10).isActive = true
        staticLabel.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -10).isActive = true

        // textField 1
        otpTextField1.topAnchor.constraint(equalTo: staticLabel.bottomAnchor, constant: 10).isActive = true
        otpTextField1.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 10).isActive = true
        otpTextField1.widthAnchor.constraint(equalToConstant: width).isActive = true
        otpTextField1.heightAnchor.constraint(equalToConstant: width).isActive = true

        // textField 2
        otpTextField2.topAnchor.constraint(equalTo: staticLabel.bottomAnchor, constant: 10).isActive = true
        otpTextField2.leadingAnchor.constraint(equalTo: otpTextField1.trailingAnchor, constant: 10).isActive = true
        otpTextField2.widthAnchor.constraint(equalTo: otpTextField1.widthAnchor).isActive = true
        otpTextField2.heightAnchor.constraint(equalToConstant: width).isActive = true

        // textField 3
        otpTextField3.topAnchor.constraint(equalTo: staticLabel.bottomAnchor, constant: 10).isActive = true
        otpTextField3.leadingAnchor.constraint(equalTo: otpTextField2.trailingAnchor, constant: 10).isActive = true
        otpTextField3.widthAnchor.constraint(equalTo: otpTextField1.widthAnchor).isActive = true
        otpTextField3.heightAnchor.constraint(equalToConstant: width).isActive = true

        // textField 4
        otpTextField4.topAnchor.constraint(equalTo: staticLabel.bottomAnchor, constant: 10).isActive = true
        otpTextField4.leadingAnchor.constraint(equalTo: otpTextField3.trailingAnchor, constant: 10).isActive = true
        otpTextField4.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -10).isActive = true
        otpTextField4.widthAnchor.constraint(equalTo: otpTextField1.widthAnchor).isActive = true
        otpTextField4.heightAnchor.constraint(equalToConstant: width).isActive = true
    

【讨论】:

@SyedFarazHaiderZaidi 我花了几个小时来解决这个问题,请支持这个和另一个答案,因为它们都有效。 点击退格键只删除最后一个文本字段,不删除其他文本字段的文本,不跳回 我不确定这个 snark 评论是干什么用的。我是帮助你的人。你可以收回投票,你还有时间。不要以为你在帮我什么忙 这不是恶评,先看看你是怎么回答我的。并将我与其他人进行比较。

以上是关于Swift - 如何在每个单独的 SMS Otp UITextfield 中放置多个字符的主要内容,如果未能解决你的问题,请参考以下文章

sms_otp_auto_verify 无法自动检测 OTP

使用 SMS Retriever API Android 的 OTP/SMS 自动获取问题

可以在 keycloak 中使用基于 SMS 的 OTP 吗?

用于 OTP 验证的 Ionic 3 读取 SMS 插件

Flutter sms_autofill 并不总是自动读取 OTP

在 IdentityServer4 和 Dotnet Core Identity 中使用带有身份验证 (oidc) 的 sms otp