// // EPLoginInputView.swift // YuMi // // Created by AI on 2025-01-27. // 登录输入框组件 - 支持区号、验证码、密码切换等完整功能 // import UIKit import SnapKit /// 输入框配置 struct EPLoginInputConfig { var showAreaCode: Bool = false var showCodeButton: Bool = false var isSecure: Bool = false var icon: String? var placeholder: String var keyboardType: UIKeyboardType = .default } /// 输入框代理 protocol EPLoginInputViewDelegate: AnyObject { func inputViewDidRequestCode(_ inputView: EPLoginInputView) func inputViewDidSelectArea(_ inputView: EPLoginInputView) } /// 登录输入框组件 class EPLoginInputView: UIView { // MARK: - Properties weak var delegate: EPLoginInputViewDelegate? /// 输入内容变化回调 var onTextChanged: ((String) -> Void)? private let stackView = UIStackView() // 区号区域 private let areaStackView = UIStackView() private let areaCodeButton = UIButton(type: .custom) private let areaArrowImageView = UIImageView() private let areaTapButton = UIButton(type: .custom) // 输入框 private let inputTextField = UITextField() private let iconImageView = UIImageView() // 眼睛按钮(密码可见性切换) private let eyeButton = UIButton(type: .custom) // 验证码按钮 private let codeButton = UIButton(type: .custom) // 倒计时 private var timer: DispatchSourceTimer? private var countdownSeconds = 60 private var isCountingDown = false // 配置 private var config: EPLoginInputConfig? /// 获取输入内容 var text: String { return inputTextField.text ?? "" } // MARK: - Initialization override init(frame: CGRect) { super.init(frame: frame) setupUI() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { stopCountdown() } // MARK: - Setup private func setupUI() { backgroundColor = EPLoginConfig.Colors.inputBackground layer.cornerRadius = EPLoginConfig.Layout.inputCornerRadius layer.borderWidth = EPLoginConfig.Layout.inputBorderWidth layer.borderColor = EPLoginConfig.Colors.inputBorder.cgColor // Main StackView stackView.axis = .horizontal stackView.alignment = .center stackView.distribution = .fill stackView.spacing = 8 stackView.translatesAutoresizingMaskIntoConstraints = false addSubview(stackView) setupAreaCodeView() setupInputTextField() setupEyeButton() setupCodeButton() stackView.snp.makeConstraints { make in make.leading.equalToSuperview().offset(EPLoginConfig.Layout.inputHorizontalPadding) make.trailing.equalToSuperview().offset(-EPLoginConfig.Layout.inputHorizontalPadding) make.top.bottom.equalToSuperview() } // 默认隐藏所有可选组件 areaStackView.isHidden = true eyeButton.isHidden = true codeButton.isHidden = true iconImageView.isHidden = true } private func setupAreaCodeView() { // 区号 StackView areaStackView.axis = .horizontal areaStackView.alignment = .center areaStackView.distribution = .fill areaStackView.spacing = 8 areaStackView.translatesAutoresizingMaskIntoConstraints = false // 区号按钮 areaCodeButton.setTitle("+86", for: .normal) areaCodeButton.setTitleColor(EPLoginConfig.Colors.inputText, for: .normal) areaCodeButton.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) areaCodeButton.isUserInteractionEnabled = false areaCodeButton.translatesAutoresizingMaskIntoConstraints = false // 箭头图标 areaArrowImageView.image = kImage("login_area_arrow") areaArrowImageView.contentMode = .scaleAspectFit areaArrowImageView.isUserInteractionEnabled = false areaArrowImageView.translatesAutoresizingMaskIntoConstraints = false // 点击区域按钮 areaTapButton.translatesAutoresizingMaskIntoConstraints = false areaTapButton.addTarget(self, action: #selector(handleAreaTap), for: .touchUpInside) areaStackView.addSubview(areaTapButton) areaStackView.addArrangedSubview(areaCodeButton) areaStackView.addArrangedSubview(areaArrowImageView) stackView.addArrangedSubview(areaStackView) areaTapButton.snp.makeConstraints { make in make.edges.equalToSuperview() } areaCodeButton.snp.makeConstraints { make in make.width.lessThanOrEqualTo(60) } areaArrowImageView.snp.makeConstraints { make in make.width.equalTo(12) make.height.equalTo(8) } } private func setupInputTextField() { // Icon (可选) iconImageView.contentMode = .scaleAspectFit iconImageView.tintColor = EPLoginConfig.Colors.icon iconImageView.translatesAutoresizingMaskIntoConstraints = false stackView.addArrangedSubview(iconImageView) iconImageView.snp.makeConstraints { make in make.size.equalTo(EPLoginConfig.Layout.inputIconSize) } // TextField inputTextField.textColor = EPLoginConfig.Colors.textLight inputTextField.font = .systemFont(ofSize: 14) inputTextField.tintColor = EPLoginConfig.Colors.textLight inputTextField.translatesAutoresizingMaskIntoConstraints = false inputTextField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged) stackView.addArrangedSubview(inputTextField) } @objc private func textFieldDidChange() { onTextChanged?(inputTextField.text ?? "") } private func setupEyeButton() { eyeButton.translatesAutoresizingMaskIntoConstraints = false eyeButton.setImage(kImage(EPLoginConfig.Images.iconPasswordUnsee), for: .normal) eyeButton.setImage(kImage(EPLoginConfig.Images.iconPasswordSee), for: .selected) eyeButton.addTarget(self, action: #selector(handleEyeTap), for: .touchUpInside) stackView.addArrangedSubview(eyeButton) eyeButton.snp.makeConstraints { make in make.size.equalTo(24) } } private func setupCodeButton() { codeButton.translatesAutoresizingMaskIntoConstraints = false codeButton.setTitle(YMLocalizedString("XPLoginInputView0"), for: .normal) codeButton.setTitleColor(.white, for: .normal) codeButton.titleLabel?.font = .systemFont(ofSize: 12, weight: .medium) codeButton.titleLabel?.textAlignment = .center codeButton.titleLabel?.numberOfLines = 2 codeButton.layer.cornerRadius = EPLoginConfig.Layout.codeButtonHeight / 2 codeButton.backgroundColor = EPLoginConfig.Colors.codeButtonBackground codeButton.addTarget(self, action: #selector(handleCodeTap), for: .touchUpInside) stackView.addArrangedSubview(codeButton) codeButton.snp.makeConstraints { make in make.width.equalTo(EPLoginConfig.Layout.codeButtonWidth) make.height.equalTo(EPLoginConfig.Layout.codeButtonHeight) } } // MARK: - Configuration /// 配置输入框 func configure(with config: EPLoginInputConfig) { self.config = config // 区号 areaStackView.isHidden = !config.showAreaCode // Icon - 默认隐藏,不再使用 iconImageView.isHidden = true // Placeholder(60% 白色) inputTextField.attributedPlaceholder = NSAttributedString( string: config.placeholder, attributes: [NSAttributedString.Key.foregroundColor: UIColor.white.withAlphaComponent(0.6)] ) // 键盘类型 inputTextField.keyboardType = config.keyboardType // 密码模式 inputTextField.isSecureTextEntry = config.isSecure eyeButton.isHidden = !config.isSecure // 验证码按钮 codeButton.isHidden = !config.showCodeButton } /// 设置区号 func setAreaCode(_ code: String) { areaCodeButton.setTitle(code, for: .normal) } /// 清空输入 func clearInput() { inputTextField.text = "" } /// 弹出键盘(自动聚焦输入框) func displayKeyboard() { inputTextField.becomeFirstResponder() } // MARK: - Actions @objc private func handleAreaTap() { delegate?.inputViewDidSelectArea(self) } @objc private func handleEyeTap() { eyeButton.isSelected.toggle() inputTextField.isSecureTextEntry = !eyeButton.isSelected } @objc private func handleCodeTap() { guard !isCountingDown else { return } delegate?.inputViewDidRequestCode(self) } // MARK: - Countdown /// 开始倒计时 func startCountdown() { guard !isCountingDown else { return } isCountingDown = true countdownSeconds = 60 codeButton.isEnabled = false codeButton.backgroundColor = EPLoginConfig.Colors.iconDisabled let queue = DispatchQueue.main let timer = DispatchSource.makeTimerSource(queue: queue) timer.schedule(deadline: .now(), repeating: 1.0) timer.setEventHandler { [weak self] in guard let self = self else { return } self.countdownSeconds -= 1 if self.countdownSeconds <= 0 { self.stopCountdown() self.codeButton.setTitle(YMLocalizedString("XPLoginInputView1"), for: .normal) } else { self.codeButton.setTitle("\(self.countdownSeconds)s", for: .normal) } } timer.resume() self.timer = timer } /// 停止倒计时 func stopCountdown() { guard let timer = timer else { return } timer.cancel() self.timer = nil isCountingDown = false codeButton.isEnabled = true codeButton.backgroundColor = EPLoginConfig.Colors.codeButtonBackground codeButton.setTitle(YMLocalizedString("XPLoginInputView0"), for: .normal) } }