I am engaged on a login display screen in my iOS app utilizing Swift. The consumer must enter their telephone quantity and press a “Ship Code” button to obtain a verification code. Here is what I am attempting to attain with the button’s state:
- The button ought to be disabled and greyed out instantly after the consumer presses it, no matter any errors.
- If there’s an error (e.g., the consumer did not enter sufficient digits), the button ought to be re-enabled after the consumer dismisses the error alert. The countdown timer shouldn’t begin on this case.
- If the consumer efficiently receives a verification code, the button ought to stay disabled for 1 minute, throughout which a countdown timer is displayed.
The explanation for disabling the button for 1 minute is to forestall the consumer from spamming the button and doubtlessly overwhelming the server with requests.
Right here is my present implementation of the view controller:
import UIKit
import FirebaseAuth
import FirebaseFunctions
class LoginViewController: BaseViewController, UITextFieldDelegate {
let phoneLabel: UILabel = {
let label = UILabel()
label.textual content = "Enter Cellphone Quantity"
label.font = UIFont.systemFont(ofSize: 24, weight: .medium)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let phoneTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Cellphone quantity"
textField.borderStyle = .roundedRect
textField.keyboardType = .numberPad
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
let sendCodeButton: UIButton = {
let button = UIButton(sort: .system)
button.setTitle("Ship code", for: .regular)
button.layer.cornerRadius = 10
button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 18)
button.isEnabled = false
button.addTarget(self, motion: #selector(sendCodeButtonTapped), for: .touchUpInside)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
let countdownLabel: UILabel = {
let label = UILabel()
label.textual content = ""
label.font = UIFont.systemFont(ofSize: 14)
label.textAlignment = .left
label.textColor = .grey
label.translatesAutoresizingMaskIntoConstraints = false
label.isHidden = true
return label
}()
lazy var features = Features.features()
var isCountdownActive = false
var countdownEndTime: Date?
override func viewDidLoad() {
tremendous.viewDidLoad()
setupUI()
updateButtonStyles()
let tapGesture = UITapGestureRecognizer(goal: self, motion: #selector(dismissKeyboard))
view.addGestureRecognizer(tapGesture)
phoneTextField.addTarget(self, motion: #selector(textFieldDidChange), for: .editingChanged)
loadCountdownState()
}
override func viewWillAppear(_ animated: Bool) {
tremendous.viewWillAppear(animated)
updateSendCodeButtonState()
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
tremendous.traitCollectionDidChange(previousTraitCollection)
if previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle {
updateButtonStyles()
}
}
non-public 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
}
non-public func updateButtonStyles() {
sendCodeButton.layer.shadowColor = UIColor.black.cgColor
sendCodeButton.layer.shadowOffset = CGSize(width: 0, peak: 2)
sendCodeButton.layer.shadowOpacity = 0.2
sendCodeButton.layer.shadowRadius = 4
}
non-public func updateSendCodeButtonState() {
let isTextEmpty = phoneTextField.textual content?.isEmpty ?? true
let isEnabled = !isTextEmpty && !isCountdownActive
sendCodeButton.isEnabled = isEnabled
let enabledColor = UIColor { traitCollection in
change traitCollection.userInterfaceStyle {
case .darkish:
return UIColor(hex: "#00CC88")
default:
return UIColor(hex: "#00C897")
}
}
let disabledColor = UIColor.lightGray
let titleColor = isEnabled ? UIColor { traitCollection in
change traitCollection.userInterfaceStyle {
case .darkish:
return UIColor(hex: "#0056B3")
default:
return UIColor(hex: "#003366")
}
} : .darkGray
sendCodeButton.backgroundColor = isEnabled ? enabledColor : disabledColor
sendCodeButton.setTitleColor(titleColor, for: .regular)
}
@objc non-public func textFieldDidChange(_ textField: UITextField) {
updateSendCodeButtonState()
}
@objc non-public func sendCodeButtonTapped() {
guard let phoneNumber = phoneTextField.textual content, phoneNumber.rely == 10 else {
showAlert(title: "Error", message: "Cellphone quantity have to be precisely 10 digits.")
sendCodeButton.isEnabled = true
return
}
let formattedPhoneNumber = "+1(phoneNumber)"
sendCodeButton.isEnabled = false
updateSendCodeButtonState()
activityIndicator.startAnimating()
features.httpsCallable("checkPhoneNumberExists").name(["phoneNumber": formattedPhoneNumber]) { [weak self] consequence, error in
self?.activityIndicator.stopAnimating()
if let error = error {
self?.showAlert(title: "Error", message: "Error checking telephone quantity: (error.localizedDescription)")
self?.sendCodeButton.isEnabled = true
return
}
guard let knowledge = consequence?.knowledge as? [String: Any], let exists = knowledge["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: "Cellphone quantity doesn't exist. Please enroll first.")
self?.sendCodeButton.isEnabled = true
}
}
}
non-public func startCountdown(seconds: Int) {
var remainingSeconds = seconds
isCountdownActive = true
countdownEndTime = Date().addingTimeInterval(TimeInterval(seconds))
UserDefaults.normal.set(countdownEndTime?.timeIntervalSince1970, forKey: "countdownEndTime")
countdownLabel.textual content = "Resend code in: (remainingSeconds)s"
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] timer in
remainingSeconds -= 1
if remainingSeconds > 0 {
self?.countdownLabel.textual content = "Resend code in: (remainingSeconds)s"
} else {
timer.invalidate()
self?.isCountdownActive = false
self?.countdownEndTime = nil
UserDefaults.normal.removeObject(forKey: "countdownEndTime")
self?.updateSendCodeButtonState()
self?.countdownLabel.isHidden = true
}
}
}
non-public func sendVerificationCode(to phoneNumber: String) {
PhoneAuthProvider.supplier().verifyPhoneNumber(phoneNumber, uiDelegate: nil) { [weak self] verificationID, error in
if let error = error {
self?.showAlert(title: "Error", message: "Unable to ship code. Please strive once more.")
print(error.localizedDescription)
self?.sendCodeButton.isEnabled = true
return
}
UserDefaults.normal.set(verificationID, forKey: "authVerificationID")
UserDefaults.normal.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: "", model: .plain, goal: nil, motion: nil)
navigationItem.backBarButtonItem = backButton
}
func textField(_ textField: UITextField, shouldChangeCharactersIn vary: NSRange, replacementString string: String) -> Bool {
let allowedCharacters = CharacterSet.decimalDigits
let characterSet = CharacterSet(charactersIn: string)
let currentText = textField.textual content ?? ""
let prospectiveText = (currentText as NSString).replacingCharacters(in: vary, with: string)
updateSendCodeButtonState()
return allowedCharacters.isSuperset(of: characterSet) && prospectiveText.rely <= 10
}
let activityIndicator: UIActivityIndicatorView = {
let indicator = UIActivityIndicatorView(model: .medium)
indicator.translatesAutoresizingMaskIntoConstraints = false
indicator.hidesWhenStopped = true
return indicator
}()
non-public func showAlert(title: String, message: String) {
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "OK", model: .default, handler: nil))
current(alertController, animated: true, completion: nil)
}
@objc func dismissKeyboard() {
view.endEditing(true)
}
non-public func loadCountdownState() {
if let endTimeInterval = UserDefaults.normal.worth(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))
}
}
}
}