486 lines
16 KiB
Swift
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()
|
|
}
|
|
}
|