// // ReceiveMessageReportPopView.swift // QuickLocation // import IQKeyboardManagerSwift import UIKit final class ReceiveMessageReportPopView: UIView { private static let shared = ReceiveMessageReportPopView( frame: CGRect(origin: .zero, size: kScreenSize) ) private let reasons = ["诈骗", "涉黄", "违规行为", "散播不良广告", "谩骂攻击", "其他"] private var selectedReasons = Set() private var optionViews: [ReceiveMessageReportOptionView] = [] private var popCenterYConstraint: NSLayoutConstraint? private var wasIQKeyboardEnabled = true static func show() { guard let window = kKeyWindow else { return } let popup = ReceiveMessageReportPopView.shared popup.resetState() popup.removeFromSuperview() window.addSubview(popup) window.bringSubviewToFront(popup) popup.bgView.alpha = 0 popup.popView.transform = CGAffineTransform(scaleX: 0.94, y: 0.94) UIView.animate(withDuration: 0.22, delay: 0, options: [.curveEaseOut]) { popup.bgView.alpha = 1 popup.popView.transform = .identity } } static func dismiss() { let popup = ReceiveMessageReportPopView.shared guard popup.superview != nil else { return } popup.endEditing(true) UIView.animate(withDuration: 0.2, delay: 0, options: [.curveEaseIn]) { popup.bgView.alpha = 0 popup.popView.transform = CGAffineTransform(scaleX: 0.94, y: 0.94) } completion: { _ in popup.removeFromSuperview() popup.resetState() } } private let bgView: UIView = { let view = UIView() view.backgroundColor = UIColor.black.withAlphaComponent(0.5) return view }() private let popView = UIView() private let popBgView: UIView = { let view = UIView() view.backgroundColor = .white view.layer.cornerRadius = 30 view.clipsToBounds = true return view }() private let logoImgView = UIImageView(image: UIImage(named: "Popup/bell")) private let headerBgImg: UIImageView = { let view = UIImageView(image: UIImage(named: "Popup/header_bg")) view.contentMode = .scaleAspectFill view.clipsToBounds = true return view }() private let titleLabel: UILabel = { let label = UILabel() label.text = "举报原因" label.font = .systemFont(ofSize: 20, weight: .bold) label.textColor = UIColor(hexStr: "#293445") label.textAlignment = .center return label }() private let optionsStack: UIStackView = { let stack = UIStackView() stack.axis = .vertical stack.spacing = 22 stack.alignment = .fill stack.distribution = .fillEqually return stack }() private let inputContainer: UIView = { let view = UIView() view.backgroundColor = UIColor(hexStr: "#F4F6FA") view.layer.cornerRadius = 16 return view }() private let remarkTextView: UITextView = { let textView = UITextView() textView.backgroundColor = .clear textView.font = .systemFont(ofSize: 15, weight: .medium) textView.textColor = UIColor(hexStr: "#293445") textView.textContainerInset = UIEdgeInsets(top: 12, left: 10, bottom: 12, right: 10) textView.textContainer.lineFragmentPadding = 0 textView.returnKeyType = .done return textView }() private let placeholderLabel: UILabel = { let label = UILabel() label.text = "请输入..." label.font = .systemFont(ofSize: 15, weight: .medium) label.textColor = UIColor(hexStr: "#C0C4CC") return label }() private let confirmBtn: UIButton = { let button = UIButton(type: .custom) button.setTitle("确定", for: .normal) button.setTitleColor(.white, for: .normal) button.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) button.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal) button.layer.cornerRadius = 25 button.clipsToBounds = true return button }() private let cancelBtn: UIButton = { let button = UIButton(type: .custom) button.setTitle("取消", for: .normal) button.setTitleColor(UIColor(hexStr: "#16B3FF"), for: .normal) button.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) return button }() override init(frame: CGRect) { super.init(frame: frame) setupUI() setupActions() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func didMoveToWindow() { super.didMoveToWindow() if window == nil { removeKeyboardObservers() IQKeyboardManager.shared.isEnabled = wasIQKeyboardEnabled } else { wasIQKeyboardEnabled = IQKeyboardManager.shared.isEnabled IQKeyboardManager.shared.isEnabled = false addKeyboardObservers() } } private func setupUI() { addSubview(bgView) addSubview(popView) popView.addSubview(popBgView) popView.addSubview(logoImgView) popBgView.addSubview(headerBgImg) popBgView.addSubview(titleLabel) popBgView.addSubview(optionsStack) popBgView.addSubview(inputContainer) inputContainer.addSubview(remarkTextView) inputContainer.addSubview(placeholderLabel) popBgView.addSubview(confirmBtn) popBgView.addSubview(cancelBtn) bgView.layoutChain.edges() popView.layoutChain .centerX() .width(315) popCenterYConstraint = popView.centerYAnchor.constraint(equalTo: centerYAnchor) popCenterYConstraint?.isActive = true popBgView.layoutChain.edges() logoImgView.layoutChain .left(16) .top(-45) .width(109) .height(109) headerBgImg.layoutChain .edges(excludingEdge: .bottom) .heightToWidth(100 / 315) titleLabel.layoutChain .top(70) .edgesHorzontal(20) optionViews = reasons.map { reason in ReceiveMessageReportOptionView(reason: reason, showsExtraHint: reason == "其他") } for row in 0..<3 { let rowStack = UIStackView() rowStack.axis = .horizontal rowStack.alignment = .fill rowStack.distribution = .fillEqually rowStack.spacing = 8 rowStack.isUserInteractionEnabled = true rowStack.addArrangedSubview(optionViews[row * 2]) rowStack.addArrangedSubview(optionViews[row * 2 + 1]) optionsStack.addArrangedSubview(rowStack) rowStack.layoutChain.height(36) } optionsStack.isUserInteractionEnabled = true optionsStack.layoutChain .topToBottomOfView(titleLabel, offset: 24) .left(24) .right(20) popBgView.bringSubviewToFront(optionsStack) popBgView.bringSubviewToFront(inputContainer) popBgView.bringSubviewToFront(confirmBtn) popBgView.bringSubviewToFront(cancelBtn) inputContainer.layoutChain .topToBottomOfView(optionsStack, offset: 16) .left(20) .right(20) .height(86) remarkTextView.layoutChain.edges() placeholderLabel.layoutChain .top(14) .left(16) .right(16) confirmBtn.layoutChain .topToBottomOfView(inputContainer, offset: 18) .left(20) .right(20) .height(50) cancelBtn.layoutChain .topToBottomOfView(confirmBtn) .left(20) .right(20) .height(50) .bottom(8) } private func setupActions() { remarkTextView.delegate = self confirmBtn.addTarget(self, action: #selector(tapConfirm), for: .touchUpInside) cancelBtn.addTarget(self, action: #selector(tapCancel), for: .touchUpInside) let tap = UITapGestureRecognizer(target: self, action: #selector(tapBackground)) tap.cancelsTouchesInView = false bgView.addGestureRecognizer(tap) for option in optionViews { option.addTarget(self, action: #selector(tapOption(_:)), for: .touchUpInside) } } private func resetState() { selectedReasons.removeAll() remarkTextView.text = "" placeholderLabel.isHidden = false optionViews.forEach { $0.isChosen = false } popCenterYConstraint?.constant = 0 updateConfirmState() } private func updateConfirmState() { let hasSelection = !selectedReasons.isEmpty let otherText = (remarkTextView.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines) let otherValid = !selectedReasons.contains("其他") || !otherText.isEmpty let enabled = hasSelection && otherValid confirmBtn.isEnabled = enabled confirmBtn.alpha = enabled ? 1 : 0.45 } @objc private func tapOption(_ sender: ReceiveMessageReportOptionView) { sender.isChosen.toggle() if sender.isChosen { selectedReasons.insert(sender.reason) } else { selectedReasons.remove(sender.reason) } updateConfirmState() } @objc private func tapConfirm() { guard confirmBtn.isEnabled else { return } endEditing(true) DLToast.show(text: "举报成功") ReceiveMessageReportPopView.dismiss() } @objc private func tapCancel() { ReceiveMessageReportPopView.dismiss() } @objc private func tapBackground() { endEditing(true) } private func addKeyboardObservers() { NotificationCenter.default.addObserver( self, selector: #selector(keyboardWillChange(_:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil ) NotificationCenter.default.addObserver( self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil ) } private func removeKeyboardObservers() { NotificationCenter.default.removeObserver( self, name: UIResponder.keyboardWillChangeFrameNotification, object: nil ) NotificationCenter.default.removeObserver( self, name: UIResponder.keyboardWillHideNotification, object: nil ) } @objc private func keyboardWillChange(_ notification: Notification) { guard let userInfo = notification.userInfo, let frame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return } let duration = (userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double) ?? 0.25 let keyboardFrame = convert(frame, from: nil) let inputFrame = inputContainer.convert(inputContainer.bounds, to: self) let overlap = inputFrame.maxY + 16 - keyboardFrame.minY popCenterYConstraint?.constant = overlap > 0 ? -overlap : 0 UIView.animate(withDuration: duration) { self.layoutIfNeeded() } } @objc private func keyboardWillHide(_ notification: Notification) { let duration = (notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double) ?? 0.25 popCenterYConstraint?.constant = 0 UIView.animate(withDuration: duration) { self.layoutIfNeeded() } } } extension ReceiveMessageReportPopView: UITextViewDelegate { func textViewDidChange(_ textView: UITextView) { placeholderLabel.isHidden = !(textView.text ?? "").isEmpty updateConfirmState() } func textView( _ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String ) -> Bool { if text == "\n" { textView.resignFirstResponder() return false } return true } } final class ReceiveMessageReportOptionView: UIButton { let reason: String var isChosen: Bool = false { didSet { boxView.image = isChosen ? Self.selectedImage : Self.normalImage } } private let boxView = UIImageView() private let reasonLabel = UILabel() init(reason: String, showsExtraHint: Bool) { self.reason = reason super.init(frame: .zero) boxView.image = Self.normalImage boxView.contentMode = .scaleAspectFit if showsExtraHint { let text = NSMutableAttributedString( string: reason, attributes: [ .font: UIFont.systemFont(ofSize: 16, weight: .medium), .foregroundColor: UIColor(hexStr: "#293445") ] ) text.append( NSAttributedString( string: " (补充理由)", attributes: [ .font: UIFont.systemFont(ofSize: 14, weight: .regular), .foregroundColor: UIColor(hexStr: "#C0C4CC") ] ) ) reasonLabel.attributedText = text } else { reasonLabel.text = reason reasonLabel.font = .systemFont(ofSize: 16, weight: .medium) reasonLabel.textColor = UIColor(hexStr: "#293445") } reasonLabel.adjustsFontSizeToFitWidth = true reasonLabel.minimumScaleFactor = 0.85 isUserInteractionEnabled = true adjustsImageWhenHighlighted = false contentHorizontalAlignment = .left addSubview(boxView) addSubview(reasonLabel) boxView.isUserInteractionEnabled = false reasonLabel.isUserInteractionEnabled = false boxView.layoutChain .left() .centerY() .width(22) .height(22) reasonLabel.layoutChain .leftToRightOfView(boxView, offset: 8) .centerY() .right(relation: .lessThanOrEqual) layoutChain.height(36) } override var intrinsicContentSize: CGSize { CGSize(width: UIView.noIntrinsicMetric, height: 36) } override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { bounds.insetBy(dx: -6, dy: -8).contains(point) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private static let boxSize = CGSize(width: 22, height: 22) private static let normalImage = UIGraphicsImageRenderer(size: boxSize).image { _ in let rect = CGRect(origin: .zero, size: boxSize).insetBy(dx: 1, dy: 1) UIColor.white.setFill() UIBezierPath(ovalIn: rect).fill() UIColor(hexStr: "#D0D5DD").setStroke() let border = UIBezierPath(ovalIn: rect) border.lineWidth = 1.5 border.stroke() } private static let selectedImage = UIGraphicsImageRenderer(size: boxSize).image { _ in let rect = CGRect(origin: .zero, size: boxSize).insetBy(dx: 1, dy: 1) UIColor(hexStr: "#16B3FF").setFill() UIBezierPath(ovalIn: rect).fill() let check = UIBezierPath() check.lineWidth = 1.8 check.lineCapStyle = .round check.lineJoinStyle = .round check.move(to: CGPoint(x: 6.2, y: 11.4)) check.addLine(to: CGPoint(x: 9.4, y: 14.6)) check.addLine(to: CGPoint(x: 16.0, y: 7.8)) UIColor.white.setStroke() check.stroke() } }