Reputation: 13
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. Here's what I'm trying to achieve with the button's state:
The reason for disabling the button for 1 minute is to prevent the user from spamming the button and potentially overwhelming the server with requests.
Here is my current implementation of the view controller:
import UIKit
import FirebaseAuth
import FirebaseFunctions
class LoginViewController: BaseViewController, UITextFieldDelegate {
let phoneLabel: UILabel = {
let label = UILabel()
label.text = "Enter Phone Number"
label.font = UIFont.systemFont(ofSize: 24, weight: .medium)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
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()
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
view.addGestureRecognizer(tapGesture)
phoneTextField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
loadCountdownState()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
updateSendCodeButtonState()
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
if previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle {
updateButtonStyles()
}
}
private func setupUI() {
title = "Log in"
view.addSubview(phoneLabel)
view.addSubview(phoneTextField)
view.addSubview(sendCodeButton)
view.addSubview(countdownLabel)
view.addSubview(activityIndicator)
NSLayoutConstraint.activate([
phoneLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
phoneLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
phoneLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
phoneTextField.topAnchor.constraint(equalTo: phoneLabel.bottomAnchor, constant: 10),
phoneTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
phoneTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
sendCodeButton.topAnchor.constraint(equalTo: phoneTextField.bottomAnchor, constant: 20),
sendCodeButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
sendCodeButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
sendCodeButton.heightAnchor.constraint(equalToConstant: 50),
countdownLabel.topAnchor.constraint(equalTo: sendCodeButton.bottomAnchor, constant: 10),
countdownLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
countdownLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
activityIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
phoneTextField.delegate = self
}
private func updateButtonStyles() {
sendCodeButton.layer.shadowColor = UIColor.black.cgColor
sendCodeButton.layer.shadowOffset = CGSize(width: 0, height: 2)
sendCodeButton.layer.shadowOpacity = 0.2
sendCodeButton.layer.shadowRadius = 4
}
private func updateSendCodeButtonState() {
let isTextEmpty = phoneTextField.text?.isEmpty ?? true
let isEnabled = !isTextEmpty && !isCountdownActive
sendCodeButton.isEnabled = isEnabled
let enabledColor = UIColor { traitCollection in
switch traitCollection.userInterfaceStyle {
case .dark:
return UIColor(hex: "#00CC88")
default:
return UIColor(hex: "#00C897")
}
}
let disabledColor = UIColor.lightGray
let titleColor = isEnabled ? UIColor { traitCollection in
switch traitCollection.userInterfaceStyle {
case .dark:
return UIColor(hex: "#0056B3")
default:
return UIColor(hex: "#003366")
}
} : .darkGray
sendCodeButton.backgroundColor = isEnabled ? enabledColor : disabledColor
sendCodeButton.setTitleColor(titleColor, for: .normal)
}
@objc private func textFieldDidChange(_ textField: UITextField) {
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 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
}
}
}
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.")
print(error.localizedDescription)
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)
}
}
func setCustomBackButton() {
let backButton = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
navigationItem.backBarButtonItem = backButton
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let allowedCharacters = CharacterSet.decimalDigits
let characterSet = CharacterSet(charactersIn: string)
let currentText = textField.text ?? ""
let prospectiveText = (currentText as NSString).replacingCharacters(in: range, with: string)
updateSendCodeButtonState()
return allowedCharacters.isSuperset(of: characterSet) && prospectiveText.count <= 10
}
let activityIndicator: UIActivityIndicatorView = {
let indicator = UIActivityIndicatorView(style: .medium)
indicator.translatesAutoresizingMaskIntoConstraints = false
indicator.hidesWhenStopped = true
return indicator
}()
private func showAlert(title: String, message: String) {
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
present(alertController, animated: true, completion: nil)
}
@objc func dismissKeyboard() {
view.endEditing(true)
}
private func loadCountdownState() {
if let endTimeInterval = UserDefaults.standard.value(forKey: "countdownEndTime") as? TimeInterval {
let endTime = Date(timeIntervalSince1970: endTimeInterval)
let remainingTime = endTime.timeIntervalSince(Date())
if remainingTime > 0 {
isCountdownActive = true
countdownLabel.isHidden = false
startCountdown(seconds: Int(remainingTime))
}
}
}
}
Upvotes: 0
Views: 32