// // 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() textView.becomeFirstResponder() } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() } // MARK: - UI private func setupUI() { view.addSubview(navBgView) view.addSubview(navView) view.addSubview(inputTextView) inputTextView.addSubview(textView) view.addSubview(countLabel) view.addSubview(confirmBtn) navBgView.layoutChain .edges(excludingEdge: .bottom) .heightToWidth(160/375) navView.layoutChain .edges(excludingEdge: .bottom) .height(kNaviHeight) inputTextView.layoutChain .topToBottomOfView(navView, 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() { textView.text = textRelay.value updateCommittedText() 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 self?.updateCommittedText() }) .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) } private func updateCommittedText() { guard !textView.isComposingText else { return } if textView.text.last == "\n" { textView.text = String(textView.text.dropLast()) textView.selectedRange = NSRange( location: (textView.text as NSString).length, length: 0 ) textView.resignFirstResponder() } let text: String if maxLength > 0 { text = textView.limitCommittedText(to: maxLength) ?? textView.text } else { text = textView.text } countLabel.text = maxLength > 0 ? "\(text.count)/\(maxLength)" : "" confirmBtn.isEnabled = !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } // MARK: - Views lazy var navBgView: UIImageView = { let iv = UIImageView() iv.image = UIImage(named: "Common/navBar_bg_2") iv.contentMode = .scaleAspectFill return iv }() lazy var navView: BaseNavigationView = { let nav = BaseNavigationView(title: titleText) nav.onBackTapped = { [weak self] in self?.dismiss(animated: true) } return nav }() 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(.white, for: .normal) btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal) btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .bold) btn.cornerRadius = 25 btn.isEnabled = false return btn }() }