You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift实现OTP重发按钮60秒倒计时功能技术求助

解决OTP重发倒计时的实现问题

我帮你调整了代码,实现60秒倒计时功能,同时优化了Timer管理和按钮状态控制,确保体验流畅:

import UIKit

class RegistrationViewController: UIViewController, UITextFieldDelegate {
    //MARK:- Outlets
    @IBOutlet weak var nameTextField: UITextField!
    @IBOutlet weak var phoneNumTextField: UITextField!
    @IBOutlet weak var emailTextField: UITextField!
    @IBOutlet weak var passwordTextField: UITextField!
    @IBOutlet weak var conformPasswordTextField: UITextField!
    @IBOutlet weak var otpTextField: UITextField!
    @IBOutlet weak var registerButton: UIButton!
    @IBOutlet weak var sendOtpButton: UIButton!
    @IBOutlet weak var otpcountLabel: UILabel!
    @IBOutlet weak var resendButn: UIButton!
    
    var otpField: Int?
    var otpTimer: Timer? // 改成可选类型,方便管理
    var totalTime = 60 // 改为60秒倒计时
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.phoneNumTextField.keyboardType = .phonePad
        otpTextField.isHidden = true
        resendButn.isHidden = true
        resendButn.setTitle("重发OTP", for: .normal) // 初始化按钮标题
    }
    
    @IBAction func registerButton(_ sender: Any) {
        guard let name = nameTextField.text, !name.isEmpty,
              let phone = phoneNumTextField.text, !phone.isEmpty,
              let password = passwordTextField.text, !password.isEmpty,
              let confirmPwd = conformPasswordTextField.text, !confirmPwd.isEmpty else {
            AlertFun.ShowAlert(title: "提示", message: "请填写所有必填字段", in: self)
            return
        }
        
        // 验证密码一致性
        if password != confirmPwd {
            AlertFun.ShowAlert(title: "提示", message: "两次输入的密码不一致", in: self)
            return
        }
        
        registerButton.isHidden = true
        sendOtpButton.isHidden = false
        otpTextField.isHidden = false
        resendButn.isHidden = false
        
        // 启动倒计时
        startOtpCountdown()
        // 调用注册接口,后端返回OTP并发送到手机
        registerService()
    }
    
    @IBAction func sendOTPButton(_ sender: Any) {
        otpService()
    }
    
    @IBAction func resendOtpButn(_ sender: Any) {
        print("重新发送OTP")
        // 重新启动倒计时
        startOtpCountdown()
        // 这里应该调用单独的发送OTP接口,而不是注册接口!
        sendOtpOnlyService()
    }
    
    // 封装倒计时启动逻辑,避免重复代码
    private func startOtpCountdown() {
        // 先停止之前的计时器,防止多个计时器同时运行
        otpTimer?.invalidate()
        totalTime = 60 // 重置倒计时时长
        updateResendButtonState(isCounting: true)
        // 启动新的计时器
        otpTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(updateCountdown), userInfo: nil, repeats: true)
    }
    
    @objc private func updateCountdown() {
        if totalTime > 0 {
            totalTime -= 1
            resendButn.setTitle("\(totalTime)秒后重发", for: .normal)
        } else {
            otpTimer?.invalidate()
            updateResendButtonState(isCounting: false)
        }
    }
    
    // 统一更新重发按钮的状态
    private func updateResendButtonState(isCounting: Bool) {
        resendButn.isEnabled = !isCounting
        if isCounting {
            resendButn.setTitle("\(totalTime)秒后重发", for: .normal)
        } else {
            resendButn.setTitle("重发OTP", for: .normal)
        }
    }
    
    //MARK:- Service part
    @objc func registerService(){
        print("执行注册请求")
        let parameters: [String: Any] = [
            "mobile_number": Int(phoneNumTextField.text ?? "") as Any,
            "email": emailTextField.text as Any,
            "password": passwordTextField.text as Any,
            "name": nameTextField.text as Any
        ]
        
        guard let url = URL(string: "https://dev.com/webservices/register") else { return }
        var req = URLRequest(url: url)
        req.httpMethod = "POST"
        req.addValue("application/json", forHTTPHeaderField: "Content-Type") // 修复拼写错误:Contet-Type -> Content-Type
        req.addValue("application/json", forHTTPHeaderField: "Accept")
        
        guard let httpBody = try? JSONSerialization.data(withJSONObject: parameters, options: []) else { return }
        req.httpBody = httpBody
        
        let session = URLSession.shared
        session.dataTask(with: req) { [weak self] data, response, error in
            guard let self = self else { return }
            if let error = error {
                print("注册请求错误: \(error)")
                return
            }
            
            guard let data = data else {
                print("未获取到注册响应数据")
                return
            }
            
            do {
                let json = try JSONSerialization.jsonObject(with: data, options: []) as! [String: Any]
                print("注册响应: \(json)")
                self.otpField = json["otp"] as? Int
                // 可选:自动填充OTP用于测试(正式环境建议移除)
                DispatchQueue.main.async {
                    self.otpTextField.text = self.otpField.map(String.init)
                }
            } catch {
                print("解析注册响应错误: \(error)")
            }
        }.resume()
    }
    
    @objc func otpService(){
        guard let otpText = otpTextField.text, !otpText.isEmpty,
              let phone = phoneNumTextField.text, !phone.isEmpty else {
            DispatchQueue.main.async {
                AlertFun.ShowAlert(title: "", message: "请输入OTP", in: self)
            }
            return
        }
        
        let parameters: [String: Any] = [
            "mobile_number": phone,
            "otp": otpText
        ]
        
        guard let url = URL(string: "https://dev.com/webservices/otpverify") else { return } // 修复多余的//
        var req = URLRequest(url: url)
        req.httpMethod = "POST"
        req.addValue("application/json", forHTTPHeaderField: "Content-Type") // 修复拼写错误
        req.addValue("application/json", forHTTPHeaderField: "Accept")
        
        guard let httpBody = try? JSONSerialization.data(withJSONObject: parameters, options: []) else { return }
        req.httpBody = httpBody
        
        let session = URLSession.shared
        session.dataTask(with: req) { [weak self] data, response, error in
            guard let self = self else { return }
            if let error = error {
                print("OTP验证请求错误: \(error)")
                return
            }
            
            guard let data = data else {
                print("未获取到OTP验证响应数据")
                return
            }
            
            do {
                let json = try JSONSerialization.jsonObject(with: data, options: []) as! [String: Any]
                print("OTP验证响应: \(json)")
                
                DispatchQueue.main.async {
                    guard let expectedOtp = self.otpField else {
                        AlertFun.ShowAlert(title: "", message: "OTP获取失败,请重试", in: self)
                        return
                    }
                    
                    if otpText == String(expectedOtp) {
                        print("注册成功")
                        let loginVC = self.storyboard?.instantiateViewController(withIdentifier: "ViewController") as! ViewController
                        self.present(loginVC, animated: true)
                    } else {
                        AlertFun.ShowAlert(title: "", message: "无效的OTP", in: self)
                    }
                }
            } catch {
                print("解析OTP验证响应错误: \(error)")
            }
        }.resume()
    }
    
    // 新增:单独的发送OTP接口(如果后端支持),否则需要调整registerService逻辑
    @objc func sendOtpOnlyService(){
        guard let phone = phoneNumTextField.text, !phone.isEmpty else { return }
        
        let parameters: [String: Any] = [
            "mobile_number": phone
        ]
        
        guard let url = URL(string: "https://dev.com/webservices/sendotp") else { return } // 假设后端有单独的发送OTP接口
        var req = URLRequest(url: url)
        req.httpMethod = "POST"
        req.addValue("application/json", forHTTPHeaderField: "Content-Type")
        req.addValue("application/json", forHTTPHeaderField: "Accept")
        
        guard let httpBody = try? JSONSerialization.data(withJSONObject: parameters, options: []) else { return }
        req.httpBody = httpBody
        
        let session = URLSession.shared
        session.dataTask(with: req) { [weak self] data, response, error in
            guard let self = self else { return }
            if let error = error {
                print("发送OTP请求错误: \(error)")
                return
            }
            
            guard let data = data else {
                print("未获取到发送OTP响应数据")
                return
            }
            
            do {
                let json = try JSONSerialization.jsonObject(with: data, options: []) as! [String: Any]
                print("发送OTP响应: \(json)")
                self.otpField = json["otp"] as? Int
                DispatchQueue.main.async {
                    self.otpTextField.text = self.otpField.map(String.init)
                }
            } catch {
                print("解析发送OTP响应错误: \(error)")
            }
        }.resume()
    }
}

关键修改说明:

  1. 倒计时时长调整:把totalTime从10改为60,符合需求的60秒倒计时
  2. Timer管理优化:
    • 将otpTimer改为可选类型,启动新计时器前先调用invalidate()停止旧计时器,避免多个计时器同时运行
    • 封装startOtpCountdown()方法,统一处理倒计时启动逻辑,减少重复代码
  3. 按钮状态控制:
    • 新增updateResendButtonState()方法,统一管理重发按钮的可点击状态和标题显示
    • 倒计时期间按钮不可点击,显示"X秒后重发";倒计时结束后恢复可点击,标题改为"重发OTP"
  4. 接口逻辑修复:
    • 修复了请求头Contet-Type的拼写错误,改为正确的Content-Type
    • 重发OTP时应该调用单独的发送OTP接口(新增sendOtpOnlyService()),而不是重复调用注册接口,避免重复创建用户
    • 增加了错误处理,比如网络请求失败、数据解析失败的情况
    • 使用[weak self]避免循环引用
  5. UI体验优化:
    • 注册前增加密码一致性验证
    • 所有UI更新操作都放在DispatchQueue.main.async中,确保线程安全

注意:如果你的后端没有单独的发送OTP接口,需要和后端沟通调整,或者修改registerService的逻辑,让它支持仅发送OTP而不重复注册用户。

内容的提问来源于stack exchange,提问作者Swift

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 08:34:34