Thursday, August 1, 2024
HomeiOS Developmentios - The way to handle the state of a "Ship Code"...

ios – The way to handle the state of a “Ship Code” button in Swift with error dealing with and countdown timer?


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:

  1. The button ought to be disabled and greyed out instantly after the consumer presses it, no matter any errors.
  2. 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.
  3. 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))
        }
        }
    }
}



Supply hyperlink

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

- Advertisment -
Google search engine

Most Popular

Recent Comments