// // TitleTextField.swift // Crush // // Created by Leon on 2025/7/18. // import SnapKit import UIKit class TitleTextField: UIView { // MARK: - Properties var titleLabel: UILabel var textfield: CLTextField var supportLabel: UILabel var placeholder: String? { didSet { textfield.placeholder = placeholder } } var errorMsg: String? var minLimit: Int = 0 var maxLimit: Int = 0 { didSet{ textfield.limit.maxCharacterNumber = maxLimit } } private var stackV: UIStackView private var tapQueryBlock: (() -> Void)? // MARK: - Initialization override init(frame: CGRect) { titleLabel = UILabel() textfield = CLTextField() supportLabel = UILabel() stackV = UIStackView() super.init(frame: frame) initialViews() setupEvent() } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // MARK: - Setup private func initialViews() { // Configure stack view stackV.axis = .vertical stackV.alignment = .leading stackV.spacing = 12 addSubview(stackV) stackV.snp.makeConstraints { make in make.top.bottom.leading.trailing.equalTo(self) } // Configure title label titleLabel.font = CLSystemToken.typography(token: .tlm).font! // EPSystemToken.typography(.txtLabelM).font titleLabel.textColor = .c.ctpn // Configure text field // Configure support label supportLabel.numberOfLines = 0 supportLabel.font = CLSystemToken.typography(token: .tbs).font! supportLabel.textColor = .c.ctsn // Default state supportLabel.isHidden = true // Add subviews to stack stackV.addArrangedSubview(titleLabel) stackV.addArrangedSubview(textfield) stackV.addArrangedSubview(supportLabel) // Additional constraints titleLabel.snp.makeConstraints { make in make.right.lessThanOrEqualTo(stackV) } textfield.snp.makeConstraints { make in make.leading.trailing.equalTo(stackV) } } private func setupEvent() { // No events in the original, kept as a placeholder textfield.addTarget(self, action: #selector(textDidChanged(_:)), for: .editingChanged) } // MARK: - Public Methods func showNormalSupportMsg(_ string: String) { supportLabel.isHidden = false supportLabel.textColor = .c.ctsn // EPSystemToken.tsn supportLabel.text = string } func showWarningMsg(_ string: String) { supportLabel.isHidden = false supportLabel.textColor = .c.cwvn // EPSystemToken.wvn supportLabel.text = string } func showErrorMsg(_ string: String) { supportLabel.isHidden = false supportLabel.textColor = .c.civn // EPSystemToken.color(.importantVariantNormal) supportLabel.text = string textfield.showAlwayErrorBorder() } func hideErrorInfo() { supportLabel.isHidden = true textfield.hideErrorBorder() } func titleAppendOptionalLabel() { // ... } func setupQueryBtn(block: @escaping () -> Void) { tapQueryBlock = block let button = UIButton(type: .custom) button.setImage(UIImage(named: "query_lightpurple"), for: .normal) button.contentMode = .center addSubview(button) let label = titleLabel button.snp.makeConstraints { make in make.centerY.equalTo(label) make.left.equalTo(label.snp.right).offset(-4) make.size.equalTo(CGSize(width: 44, height: 44)) } button.addTarget(self, action: #selector(queryButtonTapped), for: .touchUpInside) } func setupEmailMode() { textfield.autocapitalizationType = .none textfield.autocorrectionType = .no textfield.keyboardType = .emailAddress textfield.placeholder = NSLocalizedString("email", comment: "") textfield.limit.maxCharacterNumber = 50 } func setupPasswordMode() { textfield.autocapitalizationType = .none textfield.autocorrectionType = .no textfield.isSecureTextEntry = true textfield.keyboardType = .asciiCapable textfield.placeholder = NSLocalizedString("password", comment: "") textfield.limit.maxCharacterNumber = 15 textfield.setupRightEyeButton() clipsToBounds = true } func verifyPsswordDataValid(isFinalStep: Bool) -> Bool { let text = textfield.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" if text.isEmpty { if isFinalStep || !textfield.isFirstResponder { showErrorMsg(NSLocalizedString("hint_enter_password", comment: "")) } else { hideErrorInfo() } return false } if text.count < 6 || text.count > 15 { if isFinalStep || !textfield.isFirstResponder || text.count > 15 { showErrorMsg(NSLocalizedString("password_error_info", comment: "")) } else { hideErrorInfo() } return false } // Check for digits let numPattern = ".*\\d+.*" let numPred = NSPredicate(format: "SELF MATCHES %@", numPattern) if !numPred.evaluate(with: text) { print("Password must contain digits") if isFinalStep || !textfield.isFirstResponder { showErrorMsg(NSLocalizedString("toast_password_need_6_15_characters", comment: "")) } else { hideErrorInfo() } return false } // Check for lowercase letters let lowerPattern = ".*[a-z]+.*" let lowerPred = NSPredicate(format: "SELF MATCHES %@", lowerPattern) if !lowerPred.evaluate(with: text) { print("Password must contain lowercase letters") if isFinalStep || !textfield.isFirstResponder { showErrorMsg(NSLocalizedString("toast_password_need_6_15_characters", comment: "")) } else { hideErrorInfo() } return false } // Check for uppercase letters let upperPattern = ".*[A-Z]+.*" let upperPred = NSPredicate(format: "SELF MATCHES %@", upperPattern) if !upperPred.evaluate(with: text) { print("Password must contain uppercase letters") if isFinalStep || !textfield.isFirstResponder { showErrorMsg(NSLocalizedString("toast_password_need_6_15_characters", comment: "")) } else { hideErrorInfo() } return false } hideErrorInfo() return true } func verifyOldPsswordDataValid(isFinalStep: Bool) -> Bool { let text = textfield.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" if text.isEmpty { if isFinalStep || !textfield.isFirstResponder { showErrorMsg(NSLocalizedString("hint_enter_password", comment: "")) } else { hideErrorInfo() } return false } if text.count < 6 || text.count > 15 { if isFinalStep || !textfield.isFirstResponder { showErrorMsg(NSLocalizedString("password_error_info", comment: "")) } else { hideErrorInfo() } return false } hideErrorInfo() return true } func verifyEmailDataValid(isFinalStep: Bool) -> Bool { let text = textfield.text ?? "" if text.isEmpty { if isFinalStep || !textfield.isFirstResponder { showErrorMsg(NSLocalizedString("hint_enter_email", comment: "")) } else { hideErrorInfo() } return false } if !text.isValidEmail { if isFinalStep || !textfield.isFirstResponder { showErrorMsg(NSLocalizedString("email_format_incorrect", comment: "")) } else { hideErrorInfo() } return false } return true } func verifyNewStylePassword(isFinalStep: Bool) -> Bool { let text = textfield.text ?? "" let textEmpty = text.isEmpty var condition1 = true var condition2 = true var condition3 = true // Check length if text.count < 6 || text.count > 15 { condition1 = false } // Check for digits let numPattern = ".*\\d+.*" let numPred = NSPredicate(format: "SELF MATCHES %@", numPattern) if !numPred.evaluate(with: text) { print("Password must contain digits") condition2 = false } // Check for lowercase letters let lowerPattern = ".*[a-z]+.*" let lowerPred = NSPredicate(format: "SELF MATCHES %@", lowerPattern) if !lowerPred.evaluate(with: text) { print("Password must contain lowercase letters") condition3 = false } // Check for uppercase letters let upperPattern = ".*[A-Z]+.*" let upperPred = NSPredicate(format: "SELF MATCHES %@", upperPattern) if !upperPred.evaluate(with: text) { print("Password must contain uppercase letters") condition3 = false } let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.lineSpacing = 4 let normalAttributes: [NSAttributedString.Key: Any] = [ .foregroundColor: UIColor.c.ctsn, .font: CLSystemToken.typography(token: .tbs).font!, .paragraphStyle: paragraphStyle, ] let condition1String = "• \(NSLocalizedString("password_count_greater_than_6", comment: ""))" let condition2String = "• \(NSLocalizedString("password_contain_numbers", comment: ""))" let condition3String = "• \(NSLocalizedString("password_contain_upper_lower_letters", comment: ""))" let full = "\(condition1String)\n\(condition2String)\n\(condition3String)" let attributedString = NSMutableAttributedString(string: full, attributes: normalAttributes) if !textEmpty { let greenAttributes: [NSAttributedString.Key: Any] = [.foregroundColor: UIColor.c.cpvn] // EPSystemToken.color(.positiveVariantNormal) let redAttributes: [NSAttributedString.Key: Any] = [.foregroundColor: UIColor.c.civn] // EPSystemToken.color(.importantVariantNormal) let range1 = (full as NSString).range(of: condition1String) let range2 = (full as NSString).range(of: condition2String) let range3 = (full as NSString).range(of: condition3String) attributedString.addAttributes(condition1 ? greenAttributes : redAttributes, range: range1) attributedString.addAttributes(condition2 ? greenAttributes : redAttributes, range: range2) attributedString.addAttributes(condition3 ? greenAttributes : redAttributes, range: range3) } supportLabel.isHidden = false supportLabel.attributedText = attributedString return condition1 && condition2 && condition3 } // MARK: - Actions @objc private func textDidChanged(_ textField: UITextField) { if (textField.text ?? "").count < minLimit{ if let msg = errorMsg{ showErrorMsg(msg) } }else{ hideErrorInfo() } } @objc private func queryButtonTapped() { tapQueryBlock?() } // MARK: - Responder override func becomeFirstResponder() -> Bool { textfield.becomeFirstResponder() } override func resignFirstResponder() -> Bool { textfield.resignFirstResponder() } }