jsdw_ios/QuickLocation/Section/Home/ReceiveMessageReportPopView...

486 lines
16 KiB
Swift

//
// 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<String>()
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()
}
}