I’m working on a login screen in my iOS app using Swift. The user needs to enter their phone number and press a “Send Code” button to receive a verification code. How can I implement the following features into my send code button?
- The button should be disabled and greyed out immediately after the user presses it, regardless of any errors.
- If there is an error (e.g., the user didn’t enter enough digits), the button should be re-enabled after the user dismisses the error alert. The countdown timer should not start in this case.
- If the user successfully receives a verification code, the button should remain disabled for 1 minute, during which a countdown timer is displayed.
The point of this is to prevent users from spamming the “Send Code” button. Here is the relevant code:
import UIKit
import FirebaseAuth
import FirebaseFunctions
class LoginViewController: BaseViewController, UITextFieldDelegate {
let phoneTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Phone number"
textField.borderStyle = .roundedRect
textField.keyboardType = .numberPad
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
let sendCodeButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Send code", for: .normal)
button.layer.cornerRadius = 10
button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 18)
button.isEnabled = false
button.addTarget(self, action: #selector(sendCodeButtonTapped), for: .touchUpInside)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
let countdownLabel: UILabel = {
let label = UILabel()
label.text = ""
label.font = UIFont.systemFont(ofSize: 14)
label.textAlignment = .left
label.textColor = .gray
label.translatesAutoresizingMaskIntoConstraints = false
label.isHidden = true
return label
}()
lazy var functions = Functions.functions()
var isCountdownActive = false
var countdownEndTime: Date?
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
updateButtonStyles()
phoneTextField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
loadCountdownState()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
updateSendCodeButtonState()
}
@objc private func sendCodeButtonTapped() {
guard let phoneNumber = phoneTextField.text, phoneNumber.count == 10 else {
showAlert(title: "Error", message: "Phone number must be exactly 10 digits.")
sendCodeButton.isEnabled = true
return
}
let formattedPhoneNumber = "+1(phoneNumber)"
sendCodeButton.isEnabled = false
updateSendCodeButtonState()
activityIndicator.startAnimating()
functions.httpsCallable("checkPhoneNumberExists").call(["phoneNumber": formattedPhoneNumber]) { [weak self] result, error in
self?.activityIndicator.stopAnimating()
if let error = error {
self?.showAlert(title: "Error", message: "Error checking phone number: (error.localizedDescription)")
self?.sendCodeButton.isEnabled = true
return
}
guard let data = result?.data as? [String: Any], let exists = data["exists"] as? Bool else {
self?.showAlert(title: "Error", message: "Invalid response from server.")
self?.sendCodeButton.isEnabled = true
return
}
if exists {
self?.sendVerificationCode(to: formattedPhoneNumber)
} else {
self?.showAlert(title: "Error", message: "Phone number does not exist. Please sign up first.")
self?.sendCodeButton.isEnabled = true
}
}
}
private func sendVerificationCode(to phoneNumber: String) {
PhoneAuthProvider.provider().verifyPhoneNumber(phoneNumber, uiDelegate: nil) { [weak self] verificationID, error in
if let error = error {
self?.showAlert(title: "Error", message: "Unable to send code. Please try again.")
self?.sendCodeButton.isEnabled = true
return
}
UserDefaults.standard.set(verificationID, forKey: "authVerificationID")
UserDefaults.standard.set(phoneNumber, forKey: "phoneNumber")
self?.countdownLabel.isHidden = false
self?.startCountdown(seconds: 60)
let verificationVC = LoginPhoneVerificationViewController(phoneNumber: phoneNumber)
self?.navigationController?.pushViewController(verificationVC, animated: true)
}
}
private func startCountdown(seconds: Int) {
var remainingSeconds = seconds
isCountdownActive = true
countdownEndTime = Date().addingTimeInterval(TimeInterval(seconds))
UserDefaults.standard.set(countdownEndTime?.timeIntervalSince1970, forKey: "countdownEndTime")
countdownLabel.text = "Resend code in: (remainingSeconds)s"
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] timer in
remainingSeconds -= 1
if remainingSeconds > 0 {
self?.countdownLabel.text = "Resend code in: (remainingSeconds)s"
} else {
timer.invalidate()
self?.isCountdownActive = false
self?.countdownEndTime = nil
UserDefaults.standard.removeObject(forKey: "countdownEndTime")
self?.updateSendCodeButtonState()
self?.countdownLabel.isHidden = true
}