jsdw_ios/QuickLocation/Section/Mine/EmergencyContact/EmergencyContactView.swift

395 lines
12 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// EmergencyContactView.swift
// QuickLocation
//
// Created by on 2026/6/11.
//
import UIKit
import RxSwift
import RxCocoa
class EmergencyContactView: UIView {
var disposeBag = DisposeBag()
private let footerView = EmergencyContactFooterView(frame: CGRect(x: 0, y: 0, width: kScreenWidth, height: 92))
private weak var openedCell: EmergencyContactCell?
func updateCount(_ count: Int) {
let prefix = "我的紧急联系人"
let suffix = "\(count)人)"
let attr = NSMutableAttributedString(
string: prefix,
attributes: [
.font: UIFont.systemFont(ofSize: 16, weight: .semibold),
.foregroundColor: UIColor(hexStr: "#0F2846")
]
)
attr.append(NSAttributedString(
string: suffix,
attributes: [
.font: UIFont.systemFont(ofSize: 14, weight: .regular),
.foregroundColor: UIColor(hexStr: "#8D97A8")
]
))
titleLab.attributedText = attr
}
func setOpenedCell(_ cell: EmergencyContactCell?) {
if let openedCell, openedCell !== cell {
openedCell.closeSwipe(animated: true)
}
openedCell = cell
}
func closeOpenedCell() {
openedCell?.closeSwipe(animated: true)
openedCell = nil
}
private func setupRx() {
let tap = UITapGestureRecognizer(target: self, action: #selector(handleBackgroundTap))
tap.cancelsTouchesInView = false
tap.delegate = self
addGestureRecognizer(tap)
}
@objc private func handleBackgroundTap() {
closeOpenedCell()
}
private func setupUI() {
addSubview(navBgView)
addSubview(navView)
addSubview(tipsView)
addSubview(titleLab)
addSubview(tableView)
navBgView.layoutChain
.edges(excludingEdge: .bottom)
.heightToWidth(160 / 375)
navView.layoutChain
.edges(excludingEdge: .bottom)
.height(kNaviHeight)
tipsView.layoutChain
.topToBottomOfView(navView, offset: 8)
.edgesHorzontal(15)
.height(36)
titleLab.layoutChain
.topToBottomOfView(tipsView, offset: 16)
.left(15)
.right(15)
tableView.layoutChain
.topToBottomOfView(titleLab, offset: 12)
.edgesHorzontal()
.bottom(kSafeBottomMargin)
}
lazy var navBgView: UIImageView = {
let iv = UIImageView()
iv.image = UIImage(named: "Common/navBar_bg_2")
iv.contentMode = .scaleAspectFill
return iv
}()
lazy var navView: BaseNavigationView = {
BaseNavigationView(title: "紧急联系人")
}()
lazy var tipsView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hexStr: "#FFF1F1")
view.cornerRadius = 10
let icon = UIImageView(image: UIImage(named: "EmergencyContact/info"))
icon.contentMode = .scaleAspectFit
view.addSubview(icon)
icon.layoutChain
.left(12)
.centerY()
.width(14)
.height(14)
let label = UILabel()
label.text = "仅在使用SOS功能或紧急情况下会主动通知紧急联系人"
label.font = .systemFont(ofSize: 12, weight: .medium)
label.textColor = UIColor(hexStr: "#FF5A5F")
label.adjustsFontSizeToFitWidth = true
label.minimumScaleFactor = 0.8
view.addSubview(label)
label.layoutChain
.leftToRightOfView(icon, offset: 6)
.right(12)
.centerY()
return view
}()
lazy var titleLab: UILabel = {
let label = UILabel()
label.numberOfLines = 1
return label
}()
lazy var tableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .plain)
tableView.backgroundColor = .clear
tableView.separatorStyle = .none
tableView.rowHeight = 92
tableView.estimatedRowHeight = 92
tableView.bounces = true
tableView.showsVerticalScrollIndicator = false
tableView.register(EmergencyContactCell.self)
tableView.tableFooterView = footerView
return tableView
}()
override init(frame: CGRect) {
super.init(frame: .zero)
backgroundColor = UIColor(hexStr: "#F7F8FA")
setupUI()
setupRx()
updateCount(0)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension EmergencyContactView: UITableViewDelegate, UIGestureRecognizerDelegate {
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
closeOpenedCell()
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
guard let view = touch.view else { return true }
return !(view is UIControl)
}
}
// MARK: - EmergencyContactCell
class EmergencyContactCell: UITableViewCell {
var onDelete: (() -> Void)?
var onSwipeChanged: ((EmergencyContactCell, Bool) -> Void)?
private let deleteSize: CGFloat = 68
private let swipeDelegate = EmergencyContactSwipeDelegate()
private let cardInset: CGFloat = 15
private let cardGap: CGFloat = 10
private var panStartX: CGFloat = 0
private var isOpen = false
private var cardLeading: NSLayoutConstraint?
private var cardTrailing: NSLayoutConstraint?
func configure(_ model: EmergencyContactModel) {
nameLab.text = model.name
phoneLab.text = Self.maskedPhone(model.phone)
closeSwipe(animated: false)
}
func closeSwipe(animated: Bool) {
setOpen(false, animated: animated)
}
override init(style: CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
backgroundColor = .clear
contentView.backgroundColor = .clear
contentView.clipsToBounds = true
contentView.layoutMargins = .zero
preservesSuperviewLayoutMargins = false
contentView.preservesSuperviewLayoutMargins = false
setupSubviews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
closeSwipe(animated: false)
onDelete = nil
onSwipeChanged = nil
}
private func setupSubviews() {
contentView.addSubview(deleteBtn)
contentView.addSubview(cardView)
cardView.addSubview(nameLab)
cardView.addSubview(phoneLab)
cardView.addSubview(arrowIcon)
deleteBtn.layoutChain
.right(cardInset)
.centerY(cardView)
.width(deleteSize)
.height(deleteSize)
cardView.layoutChain
.top(0)
.bottom(12)
cardLeading = cardView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: cardInset)
cardTrailing = cardView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -cardInset)
cardLeading?.isActive = true
cardTrailing?.isActive = true
nameLab.layoutChain
.top(18)
.left(16)
.rightToLeftOfView(arrowIcon, offset: -8)
phoneLab.layoutChain
.topToBottomOfView(nameLab, offset: 6)
.leftToView(nameLab)
.rightToView(nameLab)
arrowIcon.layoutChain
.right(16)
.centerY()
.width(18)
.height(18)
let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
swipeDelegate.cardView = cardView
pan.delegate = swipeDelegate
cardView.addGestureRecognizer(pan)
let tap = UITapGestureRecognizer(target: self, action: #selector(handleCardTap))
cardView.addGestureRecognizer(tap)
}
@objc private func handleDelete() {
onDelete?()
}
@objc private func handleCardTap() {
if isOpen {
setOpen(false, animated: true)
onSwipeChanged?(self, false)
}
}
@objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
let translation = gesture.translation(in: cardView)
switch gesture.state {
case .began:
panStartX = currentOffset
enclosingTableView?.isScrollEnabled = false
onSwipeChanged?(self, true)
case .changed:
let next = min(0, max(-maxOffset, panStartX + translation.x))
applyOffset(next)
case .ended, .cancelled, .failed:
enclosingTableView?.isScrollEnabled = true
let velocity = gesture.velocity(in: cardView).x
let shouldOpen = velocity < -400 || currentOffset < -maxOffset * 0.45
setOpen(shouldOpen, animated: true)
onSwipeChanged?(self, shouldOpen)
default:
break
}
}
private var maxOffset: CGFloat {
deleteSize + cardGap
}
private var enclosingTableView: UITableView? {
var view: UIView? = superview
while let current = view {
if let tableView = current as? UITableView {
return tableView
}
view = current.superview
}
return nil
}
private var currentOffset: CGFloat {
(cardLeading?.constant ?? cardInset) - cardInset
}
private func applyOffset(_ offset: CGFloat) {
cardLeading?.constant = cardInset + offset
cardTrailing?.constant = -cardInset + offset
isOpen = offset <= -maxOffset + 0.5
}
private func setOpen(_ open: Bool, animated: Bool) {
let offset: CGFloat = open ? -maxOffset : 0
applyOffset(offset)
let updates = { self.contentView.layoutIfNeeded() }
if animated {
UIView.animate(withDuration: 0.22, delay: 0, options: [.curveEaseOut]) {
updates()
}
} else {
updates()
}
}
private static func maskedPhone(_ phone: String) -> String {
let digits = phone.filter { $0.isASCII && $0.isNumber }
guard digits.count >= 7 else { return phone }
return "\(digits.prefix(3)) **** \(digits.suffix(4))"
}
lazy var deleteBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.backgroundColor = UIColor(hexStr: "#FF5A5F")
btn.cornerRadius = 16
btn.setImage(UIImage(named: "EmergencyContact/trash"), for: .normal)
btn.addTarget(self, action: #selector(handleDelete), for: .touchUpInside)
return btn
}()
lazy var cardView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.cornerRadius = 16
view.layer.shadowColor = UIColor(hexStr: "#0F2846", alpha: 0.06).cgColor
view.layer.shadowOffset = CGSize(width: 0, height: 2)
view.layer.shadowOpacity = 1
view.layer.shadowRadius = 8
return view
}()
lazy var nameLab: UILabel = {
let label = UILabel()
label.textColor = UIColor(hexStr: "#0F2846")
label.font = .systemFont(ofSize: 16, weight: .semibold)
return label
}()
lazy var phoneLab: UILabel = {
let label = UILabel()
label.textColor = UIColor(hexStr: "#5C6675")
label.font = .systemFont(ofSize: 14, weight: .regular)
return label
}()
lazy var arrowIcon: UIImageView = {
let view = UIImageView(image: UIImage(named: "EmergencyContact/arrow_right"))
view.contentMode = .scaleAspectFit
return view
}()
}
private final class EmergencyContactSwipeDelegate: NSObject, UIGestureRecognizerDelegate {
weak var cardView: UIView?
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard let pan = gestureRecognizer as? UIPanGestureRecognizer else { return true }
let velocity = pan.velocity(in: cardView ?? pan.view)
return abs(velocity.x) > abs(velocity.y)
}
}