jsdw_ios/QuickLocation/Section/Common/TextInput/TextInputViewController.swift

312 lines
9.8 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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 = UIColor(hexStr: "#F5FBFB")
setupUI()
setupBinding()
setupKeyboard()
textView.becomeFirstResponder()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if textViewHeightConstraint == nil {
updateTextViewHeight()
}
}
// MARK: - UI
private func setupUI() {
view.addSubview(topBar)
topBar.addSubview(closeBtn)
topBar.addSubview(titleLabel)
view.addSubview(contentView)
contentView.addSubview(textView)
textView.addSubview(countLabel)
contentView.addSubview(confirmBtn)
//
topBar.layoutChain
.top()
.edgesHorzontal()
.height(kNaviHeight)
closeBtn.layoutChain
.bottom(12)
.left(7)
.width(24).height(24)
titleLabel.layoutChain
.centerY(closeBtn)
.centerX()
// textView +
contentView.layoutChain
.topToBottomOfView(topBar, offset: 16)
.edgesHorzontal(15)
.bottom()
//
textView.layoutChain
.top()
.edgesHorzontal()
.height(textViewMinHeight)
// textView
countLabel.layoutChain
.right(-10)
.bottom(-8)
//
confirmBtn.layoutChain
.topToBottomOfView(textView, offset: 50)
.edgesHorzontal()
.height(44)
.bottom()
}
// MARK: - Binding
private func setupBinding() {
//
textView.rx.text
.compactMap { $0 }
.subscribe(onNext: { [weak self] text in
guard let self = self else { return }
let realText = self.maxLength > 0 && text.count > self.maxLength
? String(text.prefix(self.maxLength))
: text
if realText != text {
self.textView.text = realText
}
self.textRelay.accept(realText)
})
.disposed(by: disposeBag)
//
textRelay
.map { [weak self] text in
guard let self = self, self.maxLength > 0 else { return "" }
return "\(text.count)/\(self.maxLength)"
}
.bind(to: countLabel.rx.text)
.disposed(by: disposeBag)
// +
let confirmEnabled = textRelay
.map { [weak self] text in
guard let self = self else { return false }
if self.maxLength > 0 { return !text.isEmpty && text.count <= self.maxLength }
return !text.isEmpty
}
.share(replay: 1)
confirmEnabled
.bind(to: confirmBtn.rx.isEnabled)
.disposed(by: disposeBag)
confirmEnabled
.subscribe(onNext: { [weak self] enabled in
self?.confirmBtn.backgroundColor = enabled
? UIColor(hexStr: "#16B3FF")
: UIColor(hexStr: "#CCCCCC")
})
.disposed(by: disposeBag)
//
textView.rx.text
.observe(on: MainScheduler.asyncInstance)
.subscribe(onNext: { [weak self] _ in
self?.updateTextViewHeight()
})
.disposed(by: disposeBag)
//
confirmBtn.rx.tap
.withLatestFrom(textRelay)
.subscribe(onNext: { [weak self] text in
guard let self = self else { return }
self.confirmAction?(text)
self.dismiss(animated: true)
})
.disposed(by: disposeBag)
//
closeBtn.rx.tap
.subscribe(onNext: { [weak self] _ in
self?.dismiss(animated: true)
})
.disposed(by: disposeBag)
}
// MARK: - Keyboard
private var originContentY: CGFloat = 0
private func setupKeyboard() {
NotificationCenter.default.rx.notification(UIResponder.keyboardWillShowNotification)
.subscribe(onNext: { [weak self] noti in
guard let self = self,
let userInfo = noti.userInfo,
let frame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect
else { return }
let keyboardHeight = frame.height
let duration = (userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double) ?? 0.25
// contentView = -
let offset = keyboardHeight - kSafeBottomMargin
UIView.animate(withDuration: duration) {
self.contentView.transform = CGAffineTransform(translationX: 0, y: -offset)
self.view.layoutIfNeeded()
}
})
.disposed(by: disposeBag)
NotificationCenter.default.rx.notification(UIResponder.keyboardWillHideNotification)
.subscribe(onNext: { [weak self] noti in
guard let self = self else { return }
let duration = (noti.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double) ?? 0.25
UIView.animate(withDuration: duration) {
self.contentView.transform = .identity
self.view.layoutIfNeeded()
}
})
.disposed(by: disposeBag)
}
// MARK: - TextView Height
private let textViewMinHeight: CGFloat = 150
private let textViewMaxHeight: CGFloat = 300
private var textViewHeightConstraint: NSLayoutConstraint?
private func updateTextViewHeight() {
let size = textView.sizeThatFits(CGSize(width: textView.bounds.width, height: CGFloat.greatestFiniteMagnitude))
let height = min(max(size.height, textViewMinHeight), textViewMaxHeight)
if textViewHeightConstraint == nil {
textViewHeightConstraint = textView.layoutChain.height(height)
} else {
textViewHeightConstraint?.constant = height
}
UIView.setAnimationsEnabled(false)
textView.layoutIfNeeded()
UIView.setAnimationsEnabled(true)
}
// MARK: - Views
private lazy var contentView: UIView = {
let v = UIView()
v.backgroundColor = .clear
return v
}()
private lazy var topBar: UIView = {
let v = UIView()
v.backgroundColor = .clear
return v
}()
private lazy var closeBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setImage(UIImage(named: "Common/back"), for: .normal)
btn.extendEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 30)
return btn
}()
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 17, weight: .medium)
label.textColor = UIColor(hexStr: "#333333")
label.text = titleText
label.textAlignment = .center
return label
}()
lazy var textView: UITextView = {
let tv = UITextView()
tv.font = .systemFont(ofSize: 15)
tv.textColor = UIColor(hexStr: "#333333")
tv.backgroundColor = .white
tv.cornerRadius = 4
tv.layer.borderWidth = 1
tv.layer.borderColor = ThemeManager.shared.color.lineColor.cgColor
tv.textContainerInset = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12)
tv.showsVerticalScrollIndicator = true
tv.bounces = false
tv.tintColor = UIColor(hexStr: "#16B3FF")
return tv
}()
private lazy var countLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 12)
label.textColor = UIColor(hexStr: "#BBBBBB")
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.setTitleColor(.white, for: .disabled)
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
btn.cornerRadius = 22
btn.isEnabled = false
return btn
}()
}