// // TitleView.swift // Crush // // Created by Leon on 2025/7/18. // import UIKit class TitleView: UIView { // MARK: - Properties private var stackV: UIStackView private(set)var titleLabel: LineSpaceLabel private var subTitleLabel: LineSpaceLabel private var stackSpacing: CGFloat = 16 var title: String? { didSet { titleLabel.text = title } } var subtitle: String? { didSet { if let subtitle = subtitle, !subtitle.isEmpty { subTitleLabel.isHidden = false subTitleLabel.text = subtitle } else { subTitleLabel.isHidden = true subTitleLabel.text = "" } } } var attributeSubTitle: NSAttributedString? { didSet { if let attributeSubTitle = attributeSubTitle, attributeSubTitle.length > 0 { subTitleLabel.isHidden = false subTitleLabel.text = "" subTitleLabel.attributedText = attributeSubTitle } else { subTitleLabel.isHidden = true subTitleLabel.attributedText = nil } } } var titleHidden: Bool = false { didSet { titleLabel.isHidden = titleHidden } } /// Default: 16 var optionInnerTopPadding: CGFloat = 16 { didSet { makeStackConstraint() } } /// Default: 16 var optionInnerBottomPadding: CGFloat = 16 { didSet { makeStackConstraint() } } var optionInnerLRPadding: CGFloat = 24 { didSet { makeStackConstraint() } } var optionInnerTrailingAppend: CGFloat = 0 { didSet { makeStackConstraint() } } var alwaysDarkMode: Bool = false { didSet { if alwaysDarkMode { titleLabel.textColor = .c.ctpn//EPSystemToken.darkColor(.txtPrimaryNormal) subTitleLabel.textColor = .c.ctsn//EPSystemToken.darkColor(.txtSecondaryNormal) } } } // MARK: - Initialization override init(frame: CGRect) { stackV = UIStackView() titleLabel = LineSpaceLabel() subTitleLabel = LineSpaceLabel() super.init(frame: frame) // Configure stack view stackV.spacing = stackSpacing stackV.axis = .vertical addSubview(stackV) stackV.snp.makeConstraints { make in make.top.equalToSuperview().offset(optionInnerTopPadding) make.bottom.equalToSuperview().offset(-optionInnerBottomPadding) make.leading.equalToSuperview().offset(optionInnerLRPadding) make.trailing.equalToSuperview().offset(-optionInnerLRPadding).priority(999) } // Configure title label let titleT = CLSystemToken.typography(token: .thm) titleLabel.config(titleT) titleLabel.numberOfLines = 0 titleLabel.lineBreakMode = .byWordWrapping stackV.addArrangedSubview(titleLabel) // Configure subtitle label let subTitleT = CLSystemToken.typography(token: .tbm) subTitleLabel.config(subTitleT) subTitleLabel.numberOfLines = 0 stackV.addArrangedSubview(subTitleLabel) // Theme configuration self.titleLabel.textColor = .c.ctpn //EPSystemToken.color(.txtPrimaryNormal) self.subTitleLabel.textColor = .c.ctsn //EPSystemToken.color(.txtSecondaryNormal) // Default state subTitleLabel.isHidden = true } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // MARK: - Helper Methods private func makeStackConstraint() { stackV.snp.remakeConstraints { make in make.top.equalToSuperview().offset(optionInnerTopPadding) make.bottom.lessThanOrEqualToSuperview().offset(-optionInnerBottomPadding) make.leading.equalToSuperview().offset(optionInnerLRPadding) make.trailing.equalToSuperview().offset(-self.optionInnerLRPadding - self.optionInnerTrailingAppend) } } // MARK: - Public Methods // func precalcHeight() -> CGFloat { // preCalculateHeight() // } func preCalculateHeight() -> CGFloat { let topBottomPadding = optionInnerTopPadding + optionInnerBottomPadding // Calcualte way 1: height: 36 // let titleT = CLSystemToken.typography(token: .thm) // let lineSpacing = titleT.lineHeight * 0.5 // let text = titleLabel.text ?? "" // let labelWidth = UIScreen.main.bounds.width - optionInnerLRPadding * 2 - optionInnerTrailingAppend // var titleHeight = heightForText(text, font: titleT.font!, width: labelWidth, lineSpacing: lineSpacing) // Calculate way 2: height: 33.6 let titleHeight = titleLabel.isHidden ? 0 : titleLabel.sizeThatFits(CGSize(width: UIScreen.main.bounds.width - optionInnerLRPadding * 2 - optionInnerTrailingAppend, height: .greatestFiniteMagnitude)).height // dlog("oldCalculateHeight: \(oldCalculateHeight), bouncingCalculate: \(titleHeight)") let subTitleHeight = subTitleLabel.isHidden ? 0 : subTitleLabel.sizeThatFits(CGSize(width: UIScreen.main.bounds.width - optionInnerLRPadding * 2 - optionInnerTrailingAppend, height: .greatestFiniteMagnitude)).height var total = topBottomPadding + titleHeight + subTitleHeight if titleHeight > 0 && subTitleHeight > 0 { total += stackSpacing } return ceil(total) } func setupTitleSubtitleAlignment(_ alignment: NSTextAlignment) { titleLabel.textAlignment = alignment subTitleLabel.textAlignment = alignment } // MARK: - Helper Methods func heightForText(_ text: String, font: UIFont, width: CGFloat, lineSpacing: CGFloat = 0) -> CGFloat { let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.lineBreakMode = .byWordWrapping paragraphStyle.lineSpacing = lineSpacing let attributes: [NSAttributedString.Key: Any] = [ .font: font, .paragraphStyle: paragraphStyle ] let size = CGSize(width: width, height: .greatestFiniteMagnitude) let rect = (text as NSString).boundingRect( with: size, options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: attributes, context: nil ) return ceil(rect.height) } }