// // AddMessagePopView.swift // QuickLocation // import UIKit final class AddMessagePopView: UIView { private static let shared = AddMessagePopView(frame: CGRect(origin: .zero, size: kScreenSize)) private static let primaryBlue = UIColor(hexStr: "#16B3FF") private let messages = QuickMessageView.messageList private var selectedIndex: Int = 0 private var completion: ((Int?) -> Void)? private let bgView: UIView = { let view = UIView() view.backgroundColor = .black.withAlphaComponent(0.5) return view }() private let infoView: UIView = { let view = UIView() view.backgroundColor = .white view.layer.cornerRadius = 28 view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] view.clipsToBounds = true return view }() private let titleLab: UILabel = { let label = UILabel() label.text = "实时消息" label.textColor = UIColor(hexStr: "#293445") label.font = .systemFont(ofSize: 18, weight: .bold) label.textAlignment = .center return label }() private let stackView: UIStackView = { let stack = UIStackView() stack.axis = .vertical stack.alignment = .leading stack.spacing = 12 return stack }() private lazy var cancelBtn: UIButton = { let btn = UIButton(type: .custom) btn.setTitle("取消", for: .normal) btn.setTitleColor(Self.primaryBlue, for: .normal) btn.titleLabel?.font = FontManager.boboBold(18) btn.backgroundColor = .white btn.cornerRadius = 16 btn.layer.borderWidth = 1 btn.layer.borderColor = Self.primaryBlue.cgColor btn.addTarget(self, action: #selector(cancelAction), for: .touchUpInside) return btn }() private lazy var confirmBtn: UIButton = { let btn = UIButton(type: .custom) btn.setTitle("确定", for: .normal) btn.setTitleColor(.white, for: .normal) btn.titleLabel?.font = FontManager.boboBold(18) btn.backgroundColor = Self.primaryBlue btn.cornerRadius = 16 btn.addTarget(self, action: #selector(confirmAction), for: .touchUpInside) return btn }() override init(frame: CGRect) { super.init(frame: frame) backgroundColor = .clear setupUI() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setupUI() { addSubview(bgView) addSubview(infoView) bgView.layoutChain.edges() infoView.addSubview(titleLab) infoView.addSubview(stackView) infoView.addSubview(cancelBtn) infoView.addSubview(confirmBtn) infoView.layoutChain .bottom() .edgesHorzontal() titleLab.layoutChain .top(22) .edgesHorzontal(20) .height(24) cancelBtn.layoutChain .left(20) .bottom(20 + kSafeBottomMargin) .height(44) .widthToView(confirmBtn) confirmBtn.layoutChain .leftToRightOfView(cancelBtn, offset: 12) .right(20) .bottomToView(cancelBtn) .height(44) stackView.layoutChain .topToBottomOfView(titleLab, offset: 20) .left(20) .right(20) .bottomToTopOfView(cancelBtn, offset: -20) for (index, text) in messages.enumerated() { let button = UIButton(type: .custom) button.tag = index var configuration = UIButton.Configuration.plain() configuration.title = text configuration.baseForegroundColor = UIColor(hexStr: "#293445") configuration.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 16, bottom: 10, trailing: 16) configuration.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { incoming in var outgoing = incoming outgoing.font = .systemFont(ofSize: 15, weight: .medium) return outgoing } button.configuration = configuration button.layer.cornerRadius = 10 button.layer.borderWidth = 1 button.addTarget(self, action: #selector(selectMessage(_:)), for: .touchUpInside) stackView.addArrangedSubview(button) } let tap = UITapGestureRecognizer(target: self, action: #selector(cancelAction)) tap.delegate = self addGestureRecognizer(tap) } private func resetSelection() { selectedIndex = 0 refreshButtons() updatePreview() } private func refreshButtons() { for (index, view) in stackView.arrangedSubviews.enumerated() { guard let button = view as? UIButton else { continue } let selected = index == selectedIndex var configuration = button.configuration configuration?.baseForegroundColor = selected ? Self.primaryBlue : UIColor(hexStr: "#293445") button.configuration = configuration button.backgroundColor = selected ? UIColor(hexStr: "#F3FBFF") : UIColor(hexStr: "#F5F6F8") button.layer.borderColor = (selected ? Self.primaryBlue : UIColor.clear).cgColor } } private func slideOffTransform() -> CGAffineTransform { layoutIfNeeded() return CGAffineTransform(translationX: 0, y: max(infoView.bounds.height, kScreenHeight * 0.4)) } private func updatePreview() { guard messages.indices.contains(selectedIndex) else { return } ReceivedMessagePopView.showPreview(text: messages[selectedIndex]) } @objc private func selectMessage(_ sender: UIButton) { selectedIndex = sender.tag refreshButtons() updatePreview() } @objc private func cancelAction() { completion?(nil) } @objc private func confirmAction() { completion?(selectedIndex) } } extension AddMessagePopView { static func show(completion: @escaping (Int?) -> Void) { guard let superView = kKeyWindow else { return } let pop = AddMessagePopView.shared if pop.superview != nil { pop.removeFromSuperview() } pop.frame = CGRect(x: 0, y: 0, width: kScreenWidth, height: kScreenHeight) pop.completion = { index in completion(index) AddMessagePopView.dismiss() } pop.bgView.alpha = 0 superView.addSubview(pop) superView.bringSubviewToFront(pop) pop.layoutIfNeeded() pop.resetSelection() pop.infoView.transform = pop.slideOffTransform() UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseOut) { pop.bgView.alpha = 1 pop.infoView.transform = .identity } } static func dismiss() { let pop = AddMessagePopView.shared guard pop.superview != nil else { return } ReceivedMessagePopView.dismissPreview() UIView.animate(withDuration: 0.25, delay: 0, options: [.curveEaseIn]) { pop.bgView.alpha = 0 pop.infoView.transform = pop.slideOffTransform() } completion: { _ in pop.infoView.transform = .identity pop.removeFromSuperview() } } } extension AddMessagePopView: UIGestureRecognizerDelegate { func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { touch.view == self || touch.view == bgView } } final class ReceivedMessagePopView { private static let banner: UIView = { let view = UIView() view.backgroundColor = UIColor.white.withAlphaComponent(0.92) view.layer.cornerRadius = 10 view.isHidden = true view.isUserInteractionEnabled = false view.addSubview(messageLab) messageLab.layoutChain.edges(all: 12) return view }() private static let messageLab: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 12, weight: .medium) label.textColor = UIColor(hexStr: "#0F2846") label.numberOfLines = 2 return label }() private static var hideWorkItem: DispatchWorkItem? private static var isPreview = false private static var showToken = 0 private static var didInstallConstraints = false static func showPreview(text: String) { let attr = NSAttributedString( string: text, attributes: [ .font: UIFont.systemFont(ofSize: 12, weight: .medium), .foregroundColor: UIColor(hexStr: "#0F2846") ] ) isPreview = true show(attributed: attr, autoHide: false) } static func showIncoming(nickName: String, message: String) { let name = nickName.isEmpty ? "成员" : nickName let prefix = "对你说:" let fullText = name + prefix + message let attr = NSMutableAttributedString(string: fullText) let fullRange = NSRange(location: 0, length: (fullText as NSString).length) let nameRange = NSRange(location: 0, length: (name as NSString).length) attr.addAttribute(.font, value: UIFont.systemFont(ofSize: 12, weight: .medium), range: fullRange) attr.addAttribute(.foregroundColor, value: UIColor(hexStr: "#333333"), range: fullRange) attr.addAttribute(.foregroundColor, value: UIColor(hexStr: "#16B3FF"), range: nameRange) isPreview = false show(attributed: attr, autoHide: true) } static func dismissPreview() { guard isPreview else { return } dismiss() } static func dismiss() { hideWorkItem?.cancel() hideWorkItem = nil isPreview = false guard banner.superview != nil, !banner.isHidden else { return } showToken += 1 let token = showToken UIView.animate(withDuration: 0.25, delay: 0, options: [.curveEaseIn]) { banner.transform = CGAffineTransform(translationX: 0, y: -90) banner.alpha = 0 } completion: { finished in guard finished, token == showToken else { return } banner.isHidden = true banner.alpha = 1 banner.transform = .identity } } private static func show(attributed: NSAttributedString, autoHide: Bool) { guard let window = kKeyWindow else { return } hideWorkItem?.cancel() hideWorkItem = nil showToken += 1 let token = showToken let view = banner if view.superview != window { view.removeFromSuperview() didInstallConstraints = false window.addSubview(view) } if !didInstallConstraints { view.layoutChain .top(kStatusBarHeight) .centerX() view.widthAnchor.constraint(lessThanOrEqualToConstant: kScreenWidth - 30).isActive = true didInstallConstraints = true } window.bringSubviewToFront(view) messageLab.attributedText = attributed messageLab.preferredMaxLayoutWidth = kScreenWidth - 54 let shouldAnimateIn = view.isHidden || view.alpha < 1 view.isHidden = false view.alpha = 1 if shouldAnimateIn { view.transform = CGAffineTransform(translationX: 0, y: -90) UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseOut]) { view.transform = .identity } } else { view.transform = .identity } guard autoHide else { return } let work = DispatchWorkItem { guard token == showToken else { return } dismiss() } hideWorkItem = work DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: work) } }