jsdw_ios/QuickLocation/Section/Home/AddMessagePopView.swift

350 lines
12 KiB
Swift

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