jsdw_ios/QuickLocation/Section/Home/SOS/SOSView.swift

673 lines
30 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.

//
// SOSView.swift
// QuickLocation
//
import UIKit
import SwiftyUserDefaults
enum SOSPageState {
case idle
case countdown
case sending
case active
}
final class SOSView: UIView {
var onCountdownCompleted: (() -> Void)?
var onCancelActive: (() -> Void)?
var onManageContacts: (() -> Void)?
private(set) var state: SOSPageState = .idle
let sosPracticeView = SOSPracticeView()
private let activeBackgroundView = SOSActiveBackgroundView()
private let navView = BaseNavigationView(title: "紧急求救")
private let infoCard = UIView()
private let idleView = UIView()
private let countdownView = UIView()
private let activeView = UIView()
private let idlePulseView = SOSPulseButtonView()
private let activePulseView = SOSPulseButtonView()
private let countdownRingView = SOSCountdownRingView()
private let countdownTitleLabel = UILabel()
private let countdownBodyLabel = UILabel()
private let countdownSlider = SOSSlideCancelView()
private let activeSlider = SOSSlideCancelView()
private let contactStack = UIStackView()
private let contactScrollView = UIScrollView()
private let contactPanel = UIView()
private let contactStateStack = UIStackView()
private lazy var addContactCard = makeAddContactCard()
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
setupActions()
configureContacts([])
setState(.idle)
let hasCompletedPractice = Defaults[\.sosIsPracticeList].contains(AppContextManager.shared.userId)
sosPracticeView.isHidden = hasCompletedPractice
backgroundColor = hasCompletedPractice ? UIColor(hexStr: "#F9F9F9") : .white
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setState(_ state: SOSPageState) {
self.state = state
idleView.isHidden = state != .idle
countdownView.isHidden = state != .countdown && state != .sending
activeView.isHidden = state != .active
activeBackgroundView.isHidden = state != .active
if state == .active {
activeBackgroundView.startFlashing()
} else {
activeBackgroundView.stopFlashing()
}
switch state {
case .idle:
countdownRingView.stop()
countdownSlider.configure(style: .countdown)
countdownSlider.setInteractionEnabled(true)
idlePulseView.setShowsHand(true)
idlePulseView.setAnimationEnabled(true)
idlePulseView.isUserInteractionEnabled = true
activePulseView.setAnimationEnabled(false)
case .countdown:
idlePulseView.setAnimationEnabled(false)
activePulseView.setAnimationEnabled(false)
countdownTitleLabel.attributedText = Self.cancelTitle
countdownBodyLabel.text = "10秒后会将你的SOS和位置发送到你的圈子和紧急联系人。"
countdownSlider.configure(style: .countdown)
countdownSlider.setInteractionEnabled(true)
countdownRingView.start(duration: 10)
case .sending:
countdownRingView.stop(reset: false)
countdownTitleLabel.text = "正在发送"
countdownTitleLabel.textColor = UIColor(hexStr: "#273246")
countdownBodyLabel.text = "正在发送你的SOS和位置信息请稍候。"
countdownSlider.setInteractionEnabled(false)
case .active:
countdownRingView.stop()
activeSlider.configure(style: .active)
activeSlider.setInteractionEnabled(true)
idlePulseView.setAnimationEnabled(false)
activePulseView.setShowsHand(false)
activePulseView.setAnimationEnabled(true)
activePulseView.isUserInteractionEnabled = false
}
}
func setActiveCancelEnabled(_ enabled: Bool) {
activeSlider.setInteractionEnabled(enabled)
if enabled {
activeSlider.reset(animated: false)
}
}
func configureContacts(_ contacts: [EmergencyContactModel]) {
contactStack.arrangedSubviews.forEach {
contactStack.removeArrangedSubview($0)
$0.removeFromSuperview()
}
contactPanel.isHidden = contacts.isEmpty
addContactCard.isHidden = !contacts.isEmpty
guard !contacts.isEmpty else { return }
contacts.forEach { contact in
let card = SOSEmergencyContactCard()
card.configure(contact)
card.widthAnchor.constraint(equalToConstant: 100).isActive = true
contactStack.addArrangedSubview(card)
}
}
func stopTransientActivities() {
guard state == .countdown else { return }
countdownRingView.stop()
setState(.idle)
}
private func setupUI() {
addSubview(activeBackgroundView)
activeBackgroundView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
activeBackgroundView.topAnchor.constraint(equalTo: topAnchor),
activeBackgroundView.leadingAnchor.constraint(equalTo: leadingAnchor),
activeBackgroundView.trailingAnchor.constraint(equalTo: trailingAnchor),
activeBackgroundView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
setupNavigation()
setupInfoCard()
addSubview(idleView)
addSubview(countdownView)
addSubview(activeView)
[idleView, countdownView, activeView].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
$0.topAnchor.constraint(equalTo: infoCard.bottomAnchor, constant: 6),
$0.leadingAnchor.constraint(equalTo: leadingAnchor),
$0.trailingAnchor.constraint(equalTo: trailingAnchor),
$0.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
setupIdleView()
setupCountdownView()
setupActiveView()
addSubview(sosPracticeView)
sosPracticeView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
sosPracticeView.topAnchor.constraint(equalTo: navView.bottomAnchor),
sosPracticeView.leadingAnchor.constraint(equalTo: leadingAnchor),
sosPracticeView.trailingAnchor.constraint(equalTo: trailingAnchor),
sosPracticeView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
private func setupNavigation() {
addSubview(navView)
navView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
navView.topAnchor.constraint(equalTo: topAnchor),
navView.leadingAnchor.constraint(equalTo: leadingAnchor),
navView.trailingAnchor.constraint(equalTo: trailingAnchor),
navView.heightAnchor.constraint(equalToConstant: kNaviHeight)
])
navView.titleLabel.font = .systemFont(ofSize: 17, weight: .semibold)
let navigationOffset = CGAffineTransform(translationX: 0, y: -7)
navView.titleLabel.transform = navigationOffset
navView.backBtn.transform = navigationOffset
let moreButton = UIButton(type: .custom)
moreButton.setImage(UIImage(named: "SOS/sos_nav_more"), for: .normal)
moreButton.widthAnchor.constraint(equalToConstant: 24).isActive = true
moreButton.heightAnchor.constraint(equalToConstant: 24).isActive = true
moreButton.transform = CGAffineTransform(a: 1.5, b: 0, c: 0, d: 1.5, tx: -7, ty: -7)
moreButton.accessibilityLabel = "更多"
navView.addRightButton(moreButton)
}
private func setupInfoCard() {
infoCard.backgroundColor = UIColor.white.withAlphaComponent(0.93)
infoCard.layer.cornerRadius = 14
infoCard.layer.shadowColor = UIColor.black.cgColor
infoCard.layer.shadowOpacity = 0.035
infoCard.layer.shadowRadius = 10
infoCard.layer.shadowOffset = CGSize(width: 0, height: 4)
addSubview(infoCard)
infoCard.translatesAutoresizingMaskIntoConstraints = false
let iconBackground = UIView()
infoCard.addSubview(iconBackground)
iconBackground.translatesAutoresizingMaskIntoConstraints = false
let iconView = UIImageView(image: UIImage(named: "SOS/sos_info_alert"))
iconView.contentMode = .scaleAspectFit
iconBackground.addSubview(iconView)
iconView.translatesAutoresizingMaskIntoConstraints = false
let titleLabel = UILabel()
titleLabel.text = "遇到紧急情况?一键发送求救信息"
titleLabel.textColor = UIColor(hexStr: "#273246")
titleLabel.font = .systemFont(ofSize: 12, weight: .semibold)
infoCard.addSubview(titleLabel)
titleLabel.translatesAutoresizingMaskIntoConstraints = false
let subtitleLabel = UILabel()
subtitleLabel.text = "我们将向您的紧急联系人发送包含您位置和求救信息的通知"
subtitleLabel.textColor = UIColor(hexStr: "#888888")
subtitleLabel.font = .systemFont(ofSize: 10, weight: .regular)
subtitleLabel.adjustsFontSizeToFitWidth = true
subtitleLabel.minimumScaleFactor = 0.8
infoCard.addSubview(subtitleLabel)
subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
infoCard.topAnchor.constraint(equalTo: navView.bottomAnchor, constant: 10),
infoCard.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 15),
infoCard.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -15),
infoCard.heightAnchor.constraint(equalToConstant: 48),
iconBackground.leadingAnchor.constraint(equalTo: infoCard.leadingAnchor, constant: 10),
iconBackground.centerYAnchor.constraint(equalTo: infoCard.centerYAnchor),
iconBackground.widthAnchor.constraint(equalToConstant: 30),
iconBackground.heightAnchor.constraint(equalToConstant: 30),
iconView.centerXAnchor.constraint(equalTo: iconBackground.centerXAnchor),
iconView.centerYAnchor.constraint(equalTo: iconBackground.centerYAnchor),
iconView.widthAnchor.constraint(equalToConstant: 30),
iconView.heightAnchor.constraint(equalToConstant: 30),
titleLabel.topAnchor.constraint(equalTo: infoCard.topAnchor, constant: 7),
titleLabel.leadingAnchor.constraint(equalTo: iconBackground.trailingAnchor, constant: 6),
titleLabel.trailingAnchor.constraint(equalTo: infoCard.trailingAnchor, constant: -8),
subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 5),
subtitleLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
subtitleLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor)
])
}
private func setupIdleView() {
let scrollView = makeStateScrollView(in: idleView)
let contentView = UIView()
scrollView.addSubview(contentView)
pinContentView(contentView, to: scrollView)
contentView.addSubview(idlePulseView)
idlePulseView.translatesAutoresizingMaskIntoConstraints = false
setupContactPanel()
let contactPanelHeight = contactPanel.heightAnchor.constraint(equalToConstant: 104)
contactPanelHeight.priority = .defaultHigh
contactPanelHeight.isActive = true
let addContactCardHeight = addContactCard.heightAnchor.constraint(equalToConstant: 88)
addContactCardHeight.priority = .defaultHigh
addContactCardHeight.isActive = true
contactStateStack.axis = .vertical
contactStateStack.alignment = .fill
contactStateStack.distribution = .fill
contactStateStack.addArrangedSubview(contactPanel)
contactStateStack.addArrangedSubview(addContactCard)
contentView.addSubview(contactStateStack)
contactStateStack.translatesAutoresizingMaskIntoConstraints = false
let pulseWidth = idlePulseView.widthAnchor.constraint(equalTo: contentView.widthAnchor, constant: -30)
pulseWidth.priority = .defaultHigh
NSLayoutConstraint.activate([
idlePulseView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 15),
idlePulseView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
pulseWidth,
idlePulseView.widthAnchor.constraint(lessThanOrEqualToConstant: 345),
idlePulseView.heightAnchor.constraint(equalTo: idlePulseView.widthAnchor),
contactStateStack.topAnchor.constraint(equalTo: idlePulseView.bottomAnchor, constant: 18),
contactStateStack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 15),
contactStateStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -15),
contactStateStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20)
])
}
private func setupContactPanel() {
contactPanel.backgroundColor = UIColor.white.withAlphaComponent(0.95)
contactPanel.layer.cornerRadius = 14
contactPanel.layer.shadowColor = UIColor.black.cgColor
contactPanel.layer.shadowOpacity = 0.025
contactPanel.layer.shadowRadius = 8
contactPanel.layer.shadowOffset = CGSize(width: 0, height: 3)
let contactIcon = UIImageView(image: UIImage(named: "SOS/sos_contact_alert"))
contactIcon.contentMode = .scaleAspectFit
contactPanel.addSubview(contactIcon)
contactIcon.translatesAutoresizingMaskIntoConstraints = false
let titleLabel = UILabel()
titleLabel.text = "紧急联系人"
titleLabel.textColor = UIColor(hexStr: "#273246")
titleLabel.font = .systemFont(ofSize: 14, weight: .semibold)
contactPanel.addSubview(titleLabel)
titleLabel.translatesAutoresizingMaskIntoConstraints = false
let arrow = UIImageView(image: UIImage(named: "SOS/sos_arrow_right"))
arrow.contentMode = .scaleAspectFit
contactPanel.addSubview(arrow)
arrow.translatesAutoresizingMaskIntoConstraints = false
contactScrollView.showsHorizontalScrollIndicator = false
contactScrollView.alwaysBounceHorizontal = true
contactPanel.addSubview(contactScrollView)
contactScrollView.translatesAutoresizingMaskIntoConstraints = false
contactStack.axis = .horizontal
contactStack.alignment = .fill
contactStack.distribution = .fill
contactStack.spacing = 10
contactScrollView.addSubview(contactStack)
contactStack.translatesAutoresizingMaskIntoConstraints = false
let tap = UITapGestureRecognizer(target: self, action: #selector(handleManageContacts))
contactPanel.addGestureRecognizer(tap)
NSLayoutConstraint.activate([
contactIcon.topAnchor.constraint(equalTo: contactPanel.topAnchor, constant: 13),
contactIcon.leadingAnchor.constraint(equalTo: contactPanel.leadingAnchor, constant: 12),
contactIcon.widthAnchor.constraint(equalToConstant: 16),
contactIcon.heightAnchor.constraint(equalToConstant: 16),
titleLabel.centerYAnchor.constraint(equalTo: contactIcon.centerYAnchor),
titleLabel.leadingAnchor.constraint(equalTo: contactIcon.trailingAnchor, constant: 8),
arrow.centerYAnchor.constraint(equalTo: contactIcon.centerYAnchor),
arrow.trailingAnchor.constraint(equalTo: contactPanel.trailingAnchor, constant: -12),
arrow.widthAnchor.constraint(equalToConstant: 14),
arrow.heightAnchor.constraint(equalToConstant: 14),
contactScrollView.topAnchor.constraint(equalTo: contactIcon.bottomAnchor, constant: 8),
contactScrollView.leadingAnchor.constraint(equalTo: contactPanel.leadingAnchor, constant: 12),
contactScrollView.trailingAnchor.constraint(equalTo: contactPanel.trailingAnchor, constant: -12),
contactScrollView.bottomAnchor.constraint(equalTo: contactPanel.bottomAnchor, constant: -9),
contactScrollView.heightAnchor.constraint(equalToConstant: 46),
contactStack.topAnchor.constraint(equalTo: contactScrollView.contentLayoutGuide.topAnchor),
contactStack.leadingAnchor.constraint(equalTo: contactScrollView.contentLayoutGuide.leadingAnchor),
contactStack.trailingAnchor.constraint(equalTo: contactScrollView.contentLayoutGuide.trailingAnchor),
contactStack.bottomAnchor.constraint(equalTo: contactScrollView.contentLayoutGuide.bottomAnchor),
contactStack.heightAnchor.constraint(equalTo: contactScrollView.frameLayoutGuide.heightAnchor)
])
}
private func makeAddContactCard() -> UIControl {
let control = UIControl()
control.backgroundColor = UIColor.white.withAlphaComponent(0.95)
control.layer.cornerRadius = 14
control.layer.shadowColor = UIColor.black.cgColor
control.layer.shadowOpacity = 0.025
control.layer.shadowRadius = 8
control.layer.shadowOffset = CGSize(width: 0, height: 3)
control.addTarget(self, action: #selector(handleManageContacts), for: .touchUpInside)
let iconView = UIImageView(image: UIImage(named: "SOS/add"))
iconView.contentMode = .scaleAspectFit
iconView.translatesAutoresizingMaskIntoConstraints = false
let label = UILabel()
label.text = "添加紧急联系人"
label.textColor = UIColor(hexStr: "#999999")
label.font = .systemFont(ofSize: 14, weight: .regular)
let contentStack = UIStackView(arrangedSubviews: [iconView, label])
contentStack.axis = .horizontal
contentStack.alignment = .center
contentStack.spacing = 8
control.addSubview(contentStack)
contentStack.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
contentStack.centerXAnchor.constraint(equalTo: control.centerXAnchor),
contentStack.centerYAnchor.constraint(equalTo: control.centerYAnchor),
iconView.widthAnchor.constraint(equalToConstant: 30),
iconView.heightAnchor.constraint(equalToConstant: 30)
])
return control
}
private func setupCountdownView() {
let scrollView = makeStateScrollView(in: countdownView)
let contentView = UIView()
scrollView.addSubview(contentView)
pinContentView(contentView, to: scrollView, minimumHeight: true)
contentView.addSubview(countdownRingView)
countdownRingView.translatesAutoresizingMaskIntoConstraints = false
countdownTitleLabel.attributedText = Self.cancelTitle
countdownTitleLabel.textAlignment = .center
countdownTitleLabel.font = .systemFont(ofSize: 22, weight: .regular)
contentView.addSubview(countdownTitleLabel)
countdownTitleLabel.translatesAutoresizingMaskIntoConstraints = false
countdownBodyLabel.text = "10秒后会将你的SOS和位置发送到你的圈子和紧急联系人。"
countdownBodyLabel.textColor = UIColor(hexStr: "#273246")
countdownBodyLabel.font = .systemFont(ofSize: 15, weight: .regular)
countdownBodyLabel.textAlignment = .center
countdownBodyLabel.numberOfLines = 0
contentView.addSubview(countdownBodyLabel)
countdownBodyLabel.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(countdownSlider)
countdownSlider.translatesAutoresizingMaskIntoConstraints = false
let ringWidth = countdownRingView.widthAnchor.constraint(equalTo: contentView.widthAnchor, constant: -30)
ringWidth.priority = .defaultHigh
let sliderTop = countdownSlider.topAnchor.constraint(greaterThanOrEqualTo: countdownBodyLabel.bottomAnchor, constant: 28)
NSLayoutConstraint.activate([
countdownRingView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 15),
countdownRingView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
ringWidth,
countdownRingView.widthAnchor.constraint(lessThanOrEqualToConstant: 345),
countdownRingView.heightAnchor.constraint(equalTo: countdownRingView.widthAnchor),
countdownTitleLabel.topAnchor.constraint(equalTo: countdownRingView.bottomAnchor, constant: 8),
countdownTitleLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
countdownBodyLabel.topAnchor.constraint(equalTo: countdownTitleLabel.bottomAnchor, constant: 24),
countdownBodyLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 40),
countdownBodyLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -40),
sliderTop,
countdownSlider.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 30),
countdownSlider.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -30),
countdownSlider.heightAnchor.constraint(equalToConstant: 56),
countdownSlider.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -(kSafeBottomMargin + 12))
])
}
private func setupActiveView() {
let scrollView = makeStateScrollView(in: activeView)
let contentView = UIView()
scrollView.addSubview(contentView)
pinContentView(contentView, to: scrollView, minimumHeight: true)
contentView.addSubview(activePulseView)
activePulseView.translatesAutoresizingMaskIntoConstraints = false
let titleLabel = UILabel()
titleLabel.attributedText = Self.cancelTitle
titleLabel.font = .systemFont(ofSize: 22, weight: .regular)
titleLabel.textAlignment = .center
contentView.addSubview(titleLabel)
titleLabel.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(activeSlider)
activeSlider.translatesAutoresizingMaskIntoConstraints = false
let pulseWidth = activePulseView.widthAnchor.constraint(equalTo: contentView.widthAnchor, constant: -30)
pulseWidth.priority = .defaultHigh
NSLayoutConstraint.activate([
activePulseView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 15),
activePulseView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
pulseWidth,
activePulseView.widthAnchor.constraint(lessThanOrEqualToConstant: 345),
activePulseView.heightAnchor.constraint(equalTo: activePulseView.widthAnchor),
titleLabel.topAnchor.constraint(equalTo: activePulseView.bottomAnchor, constant: 8),
titleLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
activeSlider.topAnchor.constraint(greaterThanOrEqualTo: titleLabel.bottomAnchor, constant: 40),
activeSlider.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 30),
activeSlider.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -30),
activeSlider.heightAnchor.constraint(equalToConstant: 56),
activeSlider.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -(kSafeBottomMargin + 12))
])
}
private func setupActions() {
idlePulseView.onTap = { [weak self] in
guard let self, self.state == .idle else { return }
self.setState(.countdown)
}
countdownRingView.onCompleted = { [weak self] in
guard let self, self.state == .countdown else { return }
self.setState(.sending)
self.onCountdownCompleted?()
}
countdownSlider.onCompleted = { [weak self] in
guard let self, self.state == .countdown else { return }
self.countdownRingView.stop()
self.setState(.idle)
}
activeSlider.onCompleted = { [weak self] in
guard let self, self.state == .active else { return }
self.activeSlider.setInteractionEnabled(false)
self.onCancelActive?()
}
}
private func makeStateScrollView(in container: UIView) -> UIScrollView {
let scrollView = UIScrollView()
scrollView.showsVerticalScrollIndicator = false
scrollView.alwaysBounceVertical = false
container.addSubview(scrollView)
scrollView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: container.topAnchor),
scrollView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: container.bottomAnchor)
])
return scrollView
}
private func pinContentView(_ contentView: UIView, to scrollView: UIScrollView, minimumHeight: Bool = false) {
contentView.translatesAutoresizingMaskIntoConstraints = false
var constraints = [
contentView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
contentView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
contentView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor)
]
if minimumHeight {
constraints.append(contentView.heightAnchor.constraint(greaterThanOrEqualTo: scrollView.frameLayoutGuide.heightAnchor))
}
NSLayoutConstraint.activate(constraints)
}
@objc private func handleManageContacts() {
onManageContacts?()
}
private static var cancelTitle: NSAttributedString {
let text = NSMutableAttributedString(
string: "滑动取消",
attributes: [.foregroundColor: UIColor(hexStr: "#333333")]
)
text.addAttribute(
.foregroundColor,
value: UIColor(hexStr: "#FF383C"),
range: NSRange(location: 2, length: 2)
)
return text
}
}
private final class SOSEmergencyContactCard: UIView {
private let phoneBadgeView = UIImageView(image: UIImage(named: "SOS/sos_phone_badge"))
private let nameLabel = UILabel()
private let phoneLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
layer.cornerRadius = 14
layer.borderWidth = 0.5
layer.borderColor = UIColor(hexStr: "#EDEDED").cgColor
phoneBadgeView.contentMode = .scaleAspectFit
addSubview(phoneBadgeView)
phoneBadgeView.translatesAutoresizingMaskIntoConstraints = false
nameLabel.textColor = UIColor(hexStr: "#273246")
nameLabel.font = .systemFont(ofSize: 12, weight: .medium)
nameLabel.lineBreakMode = .byTruncatingTail
addSubview(nameLabel)
nameLabel.translatesAutoresizingMaskIntoConstraints = false
phoneLabel.textColor = UIColor(hexStr: "#888888")
phoneLabel.font = .systemFont(ofSize: 10, weight: .regular)
phoneLabel.adjustsFontSizeToFitWidth = true
phoneLabel.minimumScaleFactor = 0.72
addSubview(phoneLabel)
phoneLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
phoneBadgeView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10),
phoneBadgeView.centerYAnchor.constraint(equalTo: nameLabel.centerYAnchor),
phoneBadgeView.widthAnchor.constraint(equalToConstant: 12),
phoneBadgeView.heightAnchor.constraint(equalToConstant: 12),
nameLabel.topAnchor.constraint(equalTo: topAnchor, constant: 7),
nameLabel.leadingAnchor.constraint(equalTo: phoneBadgeView.trailingAnchor, constant: 5),
nameLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -4),
phoneLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 2),
phoneLabel.leadingAnchor.constraint(equalTo: nameLabel.leadingAnchor),
phoneLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -4)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(_ contact: EmergencyContactModel) {
nameLabel.text = contact.name.isEmpty ? "未命名" : contact.name
phoneLabel.text = Self.maskedPhone(contact.phone)
}
private static func maskedPhone(_ phone: String) -> String {
let digits = phone.filter { $0.isNumber }
guard digits.count >= 7 else { return phone }
return "\(digits.prefix(3))****\(digits.suffix(4))"
}
}
private final class SOSActiveBackgroundView: UIView {
private let gradientLayer = CAGradientLayer()
private static let flashAnimationKey = "sos.active.background.flash"
override init(frame: CGRect) {
super.init(frame: frame)
gradientLayer.type = .radial
gradientLayer.colors = [
UIColor.white.cgColor,
UIColor(hexStr: "#FFF5F5").cgColor,
UIColor(hexStr: "#FFB7BA").cgColor
]
gradientLayer.locations = [0, 0.58, 1]
gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1, y: 1)
layer.addSublayer(gradientLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
gradientLayer.frame = bounds
}
func startFlashing() {
guard gradientLayer.animation(forKey: Self.flashAnimationKey) == nil else { return }
let animation = CAKeyframeAnimation(keyPath: "opacity")
animation.values = [0.42, 1.0, 0.42]
animation.keyTimes = [0, 0.5, 1]
animation.duration = 1.15
animation.repeatCount = .infinity
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
animation.isRemovedOnCompletion = false
gradientLayer.add(animation, forKey: Self.flashAnimationKey)
}
func stopFlashing() {
gradientLayer.removeAnimation(forKey: Self.flashAnimationKey)
gradientLayer.opacity = 1
}
}