Visual_Novel_iOS/crush/Crush/Src/Components/UI/TextViews/TitleTextView.swift

403 lines
14 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// TitleTextView.swift
// Crush
//
// Created by Leon on 2025/7/20.
//
import UIKit
import SnapKit
private let defaultHeightForTextView: CGFloat = 140.0
class TitleTextView: UIView, UITextViewDelegate {
// MARK: - Properties
var textDidChanged: ((CLTextView) -> Void)?
var textDidEndEditing: ((CLTextView) -> Void)?
var textDidBeginEditing: ((CLTextView) -> Void)?
var textShouldBeginEditing: ((CLTextView) -> Void)?
var maxLimit: Int = 0 {
didSet {
if maxLimit > 0 {
countLabel.isHidden = false
countLabel.text = "0/\(maxLimit)"
textView.limit.maxCharacterNumber = maxLimit
textView.snp.updateConstraints { make in
make.bottom.equalTo(textContainer).offset(-48)
}
}
}
}
var minLimit: Int = 0
var titleLabel: UILabel
var textView: CLTextView
var supportLabel: UILabel
var placeholder: String? {
didSet {
textView.placeholder = placeholder
}
}
/// countLabel
var defaultText: String?{
didSet{
textView.text = defaultText
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {[weak self] in
guard let self = self else{return}
self.subTextDidChange(self.textView)
}
}
}
var errorMsg: String?
///
var constHeightOfTextView: CGFloat = 0
private var stackV: UIStackView
private var textContainer: UIView
private var countLabel: UILabel
private var optionLabel: EPOptionFlagLabel?
private var queryButton: EPIconTertiaryButton?
private var lastText: String?
private var location: Int = 0
private var length: Int = 0
private var focusBorderColor: UIColor
private var errorBorderColor: UIColor
private var nowBorderColor: UIColor
private var errorBorderShowMode: Bool = false
private var realDefaultHeightForTextView: CGFloat
private var tapQueryButton: (() -> Void)?
// MARK: - Initialization
override init(frame: CGRect) {
titleLabel = UILabel()
textView = CLTextView()
supportLabel = UILabel()
stackV = UIStackView()
textContainer = UIView()
countLabel = UILabel()
focusBorderColor = .c.cpvn
errorBorderColor = .c.civn
nowBorderColor = .clear
realDefaultHeightForTextView = defaultHeightForTextView
super.init(frame: frame)
initialViews()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Setup
private func initialViews() {
// Configure stack view
stackV.axis = .vertical
stackV.alignment = .leading
stackV.spacing = 16
addSubview(stackV)
stackV.snp.makeConstraints { make in
make.top.bottom.leading.trailing.equalTo(self)
}
// Configure title label
titleLabel.font = CLSystemToken.font(token: .tlm)//EPSystemToken.typography(.txtLabelM).font
titleLabel.textColor = .c.ctpn
// Configure text container
textContainer.backgroundColor = .c.csen
textContainer.layer.cornerRadius = CLSystemToken.radius(token: .rs)
textContainer.layer.borderWidth = CLSystemToken.border(token: .bs)//EPSystemToken.border(.borderS)
textContainer.layer.borderColor = UIColor.clear.cgColor
// Configure text view
textView.delegate = self
textView.backgroundColor = .clear
textView.font = CLSystemToken.font(token: .tbl)//EPSystemToken.typography(.txtBodyL).font
textView.placeholderLabel.font = CLSystemToken.font(token: .tbl)
//textView.iqPlaceholderLabel?.font = EPSystemToken.typography(.txtBodyL).font
textView.placeholderTextColor = .c.cttn
textView.textColor = .c.ctpn
textContainer.addSubview(textView)
textView.snp.makeConstraints { make in
make.top.equalTo(textContainer).offset(8)
make.leading.equalTo(textContainer).offset(16)
make.trailing.equalTo(textContainer).offset(-16)
make.bottom.equalTo(textContainer).offset(-16)
make.height.greaterThanOrEqualTo(realDefaultHeightForTextView)
make.height.equalTo(defaultHeightForTextView)
}
// Update text view height based on placeholder size
textView.updateByPlaceholderSize { [weak self] rect in
guard let self = self, rect.size.height > defaultHeightForTextView else { return }
self.realDefaultHeightForTextView = rect.size.height
self.textView.snp.updateConstraints { make in
make.height.greaterThanOrEqualTo(rect.size.height)
}
}
// Configure count label
countLabel.font = CLSystemToken.font(token: .tbs)//EPSystemToken.typography(.txtBodyS).font
countLabel.textColor = .c.ctsn//EPSystemToken.color(.txtSecondaryNormal)
textContainer.addSubview(countLabel)
countLabel.snp.makeConstraints { make in
make.trailing.equalTo(textContainer).offset(-16)
make.bottom.equalTo(textContainer).offset(-16)
}
// Configure support label
supportLabel.numberOfLines = 0
supportLabel.font = .t.tbs//EPSystemToken.typography(.txtBodyS).font
supportLabel.textColor = .c.ctsn// EPSystemToken.color(.txtSecondaryNormal)
supportLabel.isHidden = true
// Add subviews to stack
stackV.addArrangedSubview(titleLabel)
stackV.addArrangedSubview(textContainer)
stackV.addArrangedSubview(supportLabel)
textContainer.snp.makeConstraints { make in
make.left.right.equalTo(stackV)
}
setupNotStrictCheck()
}
private func setupNotStrictCheck() {
textView.autocapitalizationType = .none
textView.autocorrectionType = .no
}
// MARK: - Public Methods
func showErrorMsg(_ string: String) {
supportLabel.isHidden = false
supportLabel.textColor = .c.civn//EPSystemToken.color(.importantVariantNormal)
supportLabel.text = string
textContainer.layer.borderColor = errorBorderColor.cgColor
errorBorderShowMode = true
setNeedsLayout()
}
func hideErrorMsg() {
supportLabel.isHidden = true
supportLabel.textColor = .c.ctsn
textContainer.layer.borderColor = textView.isFirstResponder ? focusBorderColor.cgColor : UIColor.clear.cgColor
errorBorderShowMode = false
setNeedsLayout()
}
// func configDefaultText(_ txt: String?) {
// guard let txt = txt else { return }
// textView.text = txt
// if !txt.removingWhitespaceAndNewlines.isEmpty {
// textViewDidChange(textView)
// }
// }
func setupTextSync(_ text: String?) {
textView.text = text
subTextDidChange(textView)
}
func titleAppendOptionalLabel() {
if optionLabel == nil {
optionLabel = {
let v = EPOptionFlagLabel()
addSubview(v )
v.snp.makeConstraints { make in
make.leading.equalTo(titleLabel.snp.trailing).offset(8)
make.centerY.equalTo(titleLabel)
}
return v
}()
}
optionLabel?.isHidden = false
}
func titleAppendQuestionIcon(_ block: @escaping () -> Void) {
if queryButton == nil {
tapQueryButton = block
queryButton = EPIconTertiaryButton(radius: .round, iconSize: .xxs, iconCode: .question)
queryButton?.touchAreaInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
queryButton?.addTarget(self, action: #selector(tapQueryBtn), for: .touchUpInside)
addSubview(queryButton!)
queryButton?.snp.makeConstraints { make in
make.leading.equalTo(titleLabel.snp.trailing).offset(8)
make.centerY.equalTo(titleLabel)
make.trailing.lessThanOrEqualTo(self).offset(-8)
}
}
queryButton?.isHidden = false
}
func setupMaxShowLineAndTruncatingTail(_ line: Int) {
textView.isScrollEnabled = false
textView.textContainer.maximumNumberOfLines = line
textView.textContainer.lineBreakMode = .byTruncatingTail
}
func setupTest() {
titleLabel.text = "Label.M"
countLabel.text = "0/100"
maxLimit = 100
}
// MARK: - Functions
private func refreshViewsAboutMinLimitOrmaxLimit(){
if maxLimit > 0 {
countLabel.text = "\(textView.text.count)/\(maxLimit)"
}
if minLimit > 0{
if textView.text.count < minLimit && textView.text.count > 0{
if let msg = errorMsg{
showErrorMsg(msg)
}else{
dlog("error msg未设置[TitleTextView]")
}
}else{
hideErrorMsg()
}
}
}
private func refreshTextViewsHeight(){
let calHeight = textView.sizeThatFits(CGSize(width: textView.bounds.width, height: .greatestFiniteMagnitude)).height
//print("calHeight: \(calHeight), realDefaultHeightForTextView: \(realDefaultHeightForTextView)")
if constHeightOfTextView > 0 && !(textView.text.isEmpty) {
textView.snp.updateConstraints { make in
make.height.equalTo(constHeightOfTextView)
}
} else if calHeight > realDefaultHeightForTextView {
if textView.bounds.height != calHeight {
textView.snp.updateConstraints { make in
make.height.equalTo(calHeight)
}
}
} else {
if textView.bounds.height != realDefaultHeightForTextView {
textView.snp.updateConstraints { make in
make.height.equalTo(realDefaultHeightForTextView)
}
}
}
layoutIfNeeded()
}
// MARK: - Actions
@objc private func tapQueryBtn() {
tapQueryButton?()
}
// MARK: - Overrides
override func becomeFirstResponder() -> Bool {
textView.becomeFirstResponder()
}
// MARK: - UITextViewDelegate
func subTextDidChange(_ textView: CLTextView) {
errorBorderShowMode = false
nowBorderColor = focusBorderColor
if textView.isFirstResponder {
textContainer.layer.borderColor = nowBorderColor.cgColor
}
// Handle text length limiting
if textView.markedTextRange == nil {
let nsTextContent = textView.text ?? ""
let existTextNum = nsTextContent.count
if existTextNum > maxLimit && maxLimit > 0 {
// Convert maxLimit - 1 to String.Index
if let stringIndex = nsTextContent.index(nsTextContent.startIndex, offsetBy: maxLimit - 1, limitedBy: nsTextContent.endIndex) {
let range = nsTextContent.rangeOfComposedCharacterSequence(at: stringIndex)
var str = String(nsTextContent.prefix(maxLimit))
// Convert range.upperBound to an Int offset for comparison
let upperBoundOffset = nsTextContent.distance(from: nsTextContent.startIndex, to: range.upperBound)
if upperBoundOffset > maxLimit {
str = String(nsTextContent.prefix(upTo: range.lowerBound))
}
textView.text = str
} else {
// Handle out-of-bounds case
textView.text = ""
}
}
lastText = textView.text
} else {
let markRange = textView.markedTextRange
let beginning = textView.beginningOfDocument
let selectionStart = markRange?.start
let selectionEnd = markRange?.end
location = selectionStart != nil ? textView.offset(from: beginning, to: selectionStart!) : 0
length = (selectionStart != nil && selectionEnd != nil) ? textView.offset(from: selectionStart!, to: selectionEnd!) : 0
}
refreshViewsAboutMinLimitOrmaxLimit()
refreshTextViewsHeight()
}
func textViewDidChange(_ textView: UITextView) {
subTextDidChange(textView as! CLTextView)
textDidChanged?(self.textView)
}
func textViewDidBeginEditing(_ textView: UITextView) {
if !errorBorderShowMode {
textContainer.layer.borderColor = focusBorderColor.cgColor
}
textDidBeginEditing?(self.textView)
}
func textViewDidEndEditing(_ textView: UITextView) {
if !errorBorderShowMode {
textContainer.layer.borderColor = UIColor.clear.cgColor
}
textDidEndEditing?(self.textView)
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if text.isEmpty || maxLimit == 0 {
return true
}
if textView.markedTextRange == nil {
if (lastText?.count ?? 0) == maxLimit {
return false
}
location = range.location
length = text.count
}
return true
}
func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
textShouldBeginEditing?(self.textView)
return textShouldBeginEditing == nil
}
}
// MARK: - Extensions
extension String {
var removingWhitespaceAndNewlines: String {
components(separatedBy: .whitespacesAndNewlines).joined()
}
}