// // TextInputViewController.swift // QuickLocation // // Created by 八条 on 2026/6/9. // import UIKit import RxSwift import RxCocoa /// 通用文本输入页面 /// 用法: /// let vc = TextInputViewController(title: "编辑昵称", maxLength: 20) { text in /// print("用户输入: \(text)") /// } /// present(vc, animated: true) final class TextInputViewController: UIViewController { private let titleText: String private let maxLength: Int private let confirmAction: ((String) -> Void)? private let disposeBag = DisposeBag() private let textRelay = BehaviorRelay(value: "") // MARK: - Init /// - Parameters: /// - title: 页面标题 /// - maxLength: 文字输入上限(0 表示不限制) /// - initialText: 初始文本,默认空 /// - confirmAction: 确定回调 init(title: String, maxLength: Int = 0, initialText: String = "", confirmAction: ((String) -> Void)? = nil) { self.titleText = title self.maxLength = maxLength self.confirmAction = confirmAction self.textRelay.accept(initialText) super.init(nibName: nil, bundle: nil) modalPresentationStyle = .fullScreen modalTransitionStyle = .coverVertical } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white setupUI() setupBinding() navTitleLabel.text = titleText textView.becomeFirstResponder() } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() } // MARK: - UI private func setupUI() { view.addSubview(navBgView) view.addSubview(navBarView) navBarView.addSubview(navTitleLabel) navBarView.addSubview(backBtn) view.addSubview(inputTextView) inputTextView.addSubview(textView) view.addSubview(countLabel) view.addSubview(confirmBtn) navBgView.layoutChain .edges(excludingEdge: .bottom) .heightToWidth(160/375) navBarView.layoutChain .edges(excludingEdge: .bottom) .height(kNaviHeight) navTitleLabel.layoutChain .top(kStatusBarHeight + 12) .centerY(backBtn) .centerX() backBtn.layoutChain .centerY(navTitleLabel) .left(15) .width(24) .height(24) inputTextView.layoutChain .topToBottomOfView(navBarView, offset: 15) .edgesHorzontal(15) // 输入框 textView.layoutChain .edgesVertical(5) .edgesHorzontal(10) countLabel.layoutChain .topToBottomOfView(inputTextView, offset: 5) .rightToView(textView) confirmBtn.layoutChain .topToBottomOfView(inputTextView, offset: 50) .edgesHorzontal(15).height(50) } // MARK: - Binding private func setupBinding() { // 输入流 Observable.merge( textView.rx.didChange.asObservable(), textView.rx.text.map { _ in () }, textView.rx.methodInvoked(#selector(UITextView.paste(_:))).map { _ in () } ) .throttle(.milliseconds(100), scheduler: MainScheduler.instance) .subscribe(onNext: { [weak self] in guard let self = self else { return } if self.textView.text.last == "\n" { self.textView.text = String(self.textView.text.dropLast()) self.textView.resignFirstResponder() return } let count = self.textView.text.count if count > self.maxLength { self.textView.text = String(self.textView.text.prefix(self.maxLength)) self.textView.selectedRange = NSRange(location: self.maxLength, length: 0) return } self.countLabel.text = "\(count)/\(self.maxLength)" }) .disposed(by: disposeBag) textRelay.asObservable() .bind(to: textView.rx.text) .disposed(by: disposeBag) // 确定 textView.rx.text.orEmpty.map { text in let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) return !trimmed.isEmpty } .bind(to: confirmBtn.rx.isEnabled) .disposed(by: disposeBag) confirmBtn.rx.tap.subscribe(onNext: { [weak self] in guard let self = self, let text = self.textView.text else { return } self.confirmAction?(text) self.dismiss(animated: true) }) .disposed(by: disposeBag) // 关闭 backBtn.rx.tap .subscribe(onNext: { [weak self] _ in self?.dismiss(animated: true) }) .disposed(by: disposeBag) } // MARK: - Views lazy var navBgView: UIImageView = { let iv = UIImageView() iv.image = UIImage(named: "Common/navBar_bg_2") iv.contentMode = .scaleAspectFill return iv }() lazy var navBarView: UIView = { let view = UIView() view.backgroundColor = .clear return view }() lazy var navTitleLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 18, weight: .medium) label.textColor = ThemeManager.shared.color.titleAuxColor label.textAlignment = .center return label }() lazy var backBtn: UIButton = { let btn = UIButton(type: .custom) btn.setImage(UIImage(named: "Common/back"), for: .normal) btn.extendEdgeInsets = UIEdgeInsets(top: 54, left: 15, bottom: 100, right: 100) return btn }() lazy var inputTextView: UIView = { let view = UIView() view.backgroundColor = .white view.cornerRadius = 4 view.borderWidth = 0.5 view.borderColor = ThemeManager.shared.color.lineColor return view }() lazy var textView: UITextView = { let tv = UITextView() tv.font = .systemFont(ofSize: 15) tv.textColor = ThemeManager.shared.color.titleAuxColor tv.backgroundColor = .clear tv.showsVerticalScrollIndicator = false tv.isScrollEnabled = false tv.bounces = false tv.returnKeyType = .done return tv }() private lazy var countLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 13) label.textColor = UIColor(hexStr: "#999999") label.text = maxLength > 0 ? "0/\(maxLength)" : "" return label }() private lazy var confirmBtn: UIButton = { let btn = UIButton(type: .custom) btn.setTitle("确定", for: .normal) btn.setTitleColor(UIColor(hexStr: "#0F2846"), for: .normal) btn.setBackgroundImage(UIImage(named: "Common/gradient_bg"), for: .normal) btn.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium) btn.cornerRadius = 25 btn.isEnabled = false return btn }() }