242 lines
7.3 KiB
Swift
242 lines
7.3 KiB
Swift
//
|
||
// 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<String>(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
|
||
}()
|
||
}
|