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

519 lines
23 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.

//
// SOSPracticeView.swift
// QuickLocation
//
import UIKit
import SwiftyUserDefaults
final class SOSPracticeView: UIView, UIScrollViewDelegate {
private enum State {
case guide
case practiceIdle
case practiceCountdown
case success
}
private let guideView = UIView()
private let practiceIdleView = UIView()
private let practiceCountdownView = UIView()
private let successView = UIView()
private let guideScrollView = UIScrollView()
private let pageControl = UIPageControl()
private let practicePulseView = SOSPulseButtonView()
private let practiceCountdownRing = SOSCountdownRingView()
private let practiceSlider = SOSSlideCancelView(thumbImageName: "SOS/slider")
private var state: State = .guide
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
setupUI()
setupActions()
setState(.guide)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func didMoveToWindow() {
super.didMoveToWindow()
if window == nil {
practiceCountdownRing.stop()
}
}
private func setupUI() {
[guideView, practiceIdleView, practiceCountdownView, successView].forEach {
addSubview($0)
$0.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
$0.topAnchor.constraint(equalTo: topAnchor),
$0.leadingAnchor.constraint(equalTo: leadingAnchor),
$0.trailingAnchor.constraint(equalTo: trailingAnchor),
$0.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
setupGuideView()
setupPracticeIdleView()
setupPracticeCountdownView()
setupSuccessView()
}
private func setupGuideView() {
guideScrollView.isPagingEnabled = true
guideScrollView.showsHorizontalScrollIndicator = false
guideScrollView.bounces = false
guideScrollView.delegate = self
guideView.addSubview(guideScrollView)
guideScrollView.translatesAutoresizingMaskIntoConstraints = false
let pageStack = UIStackView()
pageStack.axis = .horizontal
pageStack.alignment = .fill
pageStack.distribution = .fill
pageStack.spacing = 0
guideScrollView.addSubview(pageStack)
pageStack.translatesAutoresizingMaskIntoConstraints = false
for index in 1...3 {
let page = makeGuidePage(index: index)
pageStack.addArrangedSubview(page)
page.widthAnchor.constraint(equalTo: guideScrollView.frameLayoutGuide.widthAnchor).isActive = true
}
pageControl.numberOfPages = 3
pageControl.currentPage = 0
pageControl.currentPageIndicatorTintColor = UIColor(hexStr: "#4CC7F8")
pageControl.pageIndicatorTintColor = UIColor(hexStr: "#D9D9D9")
pageControl.isUserInteractionEnabled = false
guideView.addSubview(pageControl)
pageControl.translatesAutoresizingMaskIntoConstraints = false
let startButton = UIButton(type: .custom)
startButton.setTitle("开始练习", for: .normal)
startButton.setTitleColor(.white, for: .normal)
startButton.titleLabel?.font = FontManager.boboBold(18)
startButton.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
startButton.cornerRadius = 20
startButton.addTarget(self, action: #selector(startPractice), for: .touchUpInside)
guideView.addSubview(startButton)
startButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
guideScrollView.topAnchor.constraint(equalTo: guideView.topAnchor),
guideScrollView.leadingAnchor.constraint(equalTo: guideView.leadingAnchor),
guideScrollView.trailingAnchor.constraint(equalTo: guideView.trailingAnchor),
guideScrollView.bottomAnchor.constraint(equalTo: pageControl.topAnchor, constant: -8),
pageStack.topAnchor.constraint(equalTo: guideScrollView.contentLayoutGuide.topAnchor),
pageStack.leadingAnchor.constraint(equalTo: guideScrollView.contentLayoutGuide.leadingAnchor),
pageStack.trailingAnchor.constraint(equalTo: guideScrollView.contentLayoutGuide.trailingAnchor),
pageStack.bottomAnchor.constraint(equalTo: guideScrollView.contentLayoutGuide.bottomAnchor),
pageStack.heightAnchor.constraint(equalTo: guideScrollView.frameLayoutGuide.heightAnchor),
pageControl.centerXAnchor.constraint(equalTo: guideView.centerXAnchor),
pageControl.bottomAnchor.constraint(equalTo: startButton.topAnchor, constant: -14),
startButton.leadingAnchor.constraint(equalTo: guideView.leadingAnchor, constant: 30),
startButton.trailingAnchor.constraint(equalTo: guideView.trailingAnchor, constant: -30),
startButton.heightAnchor.constraint(equalToConstant: 56),
startButton.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -20)
])
}
private func makeGuidePage(index: Int) -> UIView {
let page = UIView()
let verticalScrollView = UIScrollView()
verticalScrollView.showsVerticalScrollIndicator = false
verticalScrollView.alwaysBounceVertical = false
page.addSubview(verticalScrollView)
verticalScrollView.translatesAutoresizingMaskIntoConstraints = false
let contentView = UIView()
verticalScrollView.addSubview(contentView)
contentView.translatesAutoresizingMaskIntoConstraints = false
let imageView = UIImageView(image: UIImage(named: "SOS/sos_guide_\(index)"))
imageView.contentMode = .scaleAspectFit
contentView.addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
let bulletList = makeBulletList()
contentView.addSubview(bulletList)
bulletList.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
verticalScrollView.topAnchor.constraint(equalTo: page.topAnchor),
verticalScrollView.leadingAnchor.constraint(equalTo: page.leadingAnchor),
verticalScrollView.trailingAnchor.constraint(equalTo: page.trailingAnchor),
verticalScrollView.bottomAnchor.constraint(equalTo: page.bottomAnchor),
contentView.topAnchor.constraint(equalTo: verticalScrollView.contentLayoutGuide.topAnchor),
contentView.leadingAnchor.constraint(equalTo: verticalScrollView.contentLayoutGuide.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: verticalScrollView.contentLayoutGuide.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: verticalScrollView.contentLayoutGuide.bottomAnchor),
contentView.widthAnchor.constraint(equalTo: verticalScrollView.frameLayoutGuide.widthAnchor),
contentView.heightAnchor.constraint(greaterThanOrEqualTo: verticalScrollView.frameLayoutGuide.heightAnchor),
imageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 94),
imageView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
imageView.widthAnchor.constraint(equalToConstant: 240),
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor),
bulletList.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 28),
bulletList.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 45),
bulletList.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -30),
bulletList.bottomAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: -16)
])
return page
}
private func makeBulletList() -> UIStackView {
let stack = UIStackView()
stack.axis = .vertical
stack.alignment = .leading
stack.distribution = .fillEqually
stack.spacing = 10
["夜间独自行走", "遇见突发危机事件", "遇到医疗紧急情况"].forEach { text in
let label = UILabel()
label.textColor = UIColor(hexStr: "#333333")
label.font = .systemFont(ofSize: 16, weight: .regular)
let attributed = NSMutableAttributedString(string: "\(text)")
attributed.addAttribute(
.foregroundColor,
value: UIColor(hexStr: "#11B8F4"),
range: NSRange(location: 0, length: 1)
)
label.attributedText = attributed
stack.addArrangedSubview(label)
}
return stack
}
private func setupPracticeIdleView() {
let contentView = makeVerticalContentView(in: practiceIdleView)
let modeBadge = SOSPracticeModeBadge()
contentView.addSubview(modeBadge)
modeBadge.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(practicePulseView)
practicePulseView.translatesAutoresizingMaskIntoConstraints = false
let hintLabel = UILabel()
hintLabel.textAlignment = .center
hintLabel.font = .systemFont(ofSize: 20, weight: .regular)
let hint = NSMutableAttributedString(
string: "轻点感叹号发送SOS",
attributes: [.foregroundColor: UIColor(hexStr: "#333333")]
)
hint.addAttribute(
.foregroundColor,
value: UIColor(hexStr: "#FF383C"),
range: NSRange(location: 7, length: 3)
)
hintLabel.attributedText = hint
contentView.addSubview(hintLabel)
hintLabel.translatesAutoresizingMaskIntoConstraints = false
let pulseWidth = practicePulseView.widthAnchor.constraint(equalTo: contentView.widthAnchor, constant: -30)
pulseWidth.priority = .defaultHigh
NSLayoutConstraint.activate([
modeBadge.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 28),
modeBadge.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
modeBadge.widthAnchor.constraint(equalToConstant: 139),
modeBadge.heightAnchor.constraint(equalToConstant: 40),
practicePulseView.topAnchor.constraint(equalTo: modeBadge.bottomAnchor, constant: 18),
practicePulseView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
pulseWidth,
practicePulseView.widthAnchor.constraint(lessThanOrEqualToConstant: 345),
practicePulseView.heightAnchor.constraint(equalTo: practicePulseView.widthAnchor),
hintLabel.topAnchor.constraint(equalTo: practicePulseView.bottomAnchor, constant: 16),
hintLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
hintLabel.bottomAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: -20)
])
}
private func setupPracticeCountdownView() {
let contentView = makeVerticalContentView(in: practiceCountdownView)
let modeBadge = SOSPracticeModeBadge()
contentView.addSubview(modeBadge)
modeBadge.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(practiceCountdownRing)
practiceCountdownRing.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
let bodyLabel = UILabel()
bodyLabel.text = "10秒后会将你的SOS和位置发送到你的圈子和紧急联系人。"
bodyLabel.textColor = UIColor(hexStr: "#273246")
bodyLabel.font = .systemFont(ofSize: 15, weight: .regular)
bodyLabel.textAlignment = .center
bodyLabel.numberOfLines = 0
contentView.addSubview(bodyLabel)
bodyLabel.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(practiceSlider)
practiceSlider.translatesAutoresizingMaskIntoConstraints = false
let ringWidth = practiceCountdownRing.widthAnchor.constraint(equalTo: contentView.widthAnchor, constant: -30)
ringWidth.priority = .defaultHigh
NSLayoutConstraint.activate([
modeBadge.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 28),
modeBadge.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
modeBadge.widthAnchor.constraint(equalToConstant: 139),
modeBadge.heightAnchor.constraint(equalToConstant: 40),
practiceCountdownRing.topAnchor.constraint(equalTo: modeBadge.bottomAnchor, constant: 8),
practiceCountdownRing.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
ringWidth,
practiceCountdownRing.widthAnchor.constraint(lessThanOrEqualToConstant: 345),
practiceCountdownRing.heightAnchor.constraint(equalTo: practiceCountdownRing.widthAnchor),
titleLabel.topAnchor.constraint(equalTo: practiceCountdownRing.bottomAnchor, constant: 2),
titleLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
bodyLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 24),
bodyLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 40),
bodyLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -40),
practiceSlider.topAnchor.constraint(greaterThanOrEqualTo: bodyLabel.bottomAnchor, constant: 28),
practiceSlider.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 30),
practiceSlider.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -30),
practiceSlider.heightAnchor.constraint(equalToConstant: 56),
practiceSlider.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -(kSafeBottomMargin + 20))
])
}
private func setupSuccessView() {
let contentView = makeVerticalContentView(in: successView)
let modeBadge = SOSPracticeModeBadge()
contentView.addSubview(modeBadge)
modeBadge.translatesAutoresizingMaskIntoConstraints = false
let imageView = UIImageView(image: UIImage(named: "SOS/sos_practice_success"))
imageView.contentMode = .scaleAspectFit
contentView.addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
let titleLabel = UILabel()
titleLabel.textAlignment = .center
titleLabel.font = .systemFont(ofSize: 22, weight: .regular)
let title = NSMutableAttributedString(
string: "求救成功",
attributes: [.foregroundColor: UIColor(hexStr: "#333333")]
)
title.addAttribute(
.foregroundColor,
value: UIColor(hexStr: "#FF383C"),
range: NSRange(location: 2, length: 2)
)
titleLabel.attributedText = title
contentView.addSubview(titleLabel)
titleLabel.translatesAutoresizingMaskIntoConstraints = false
let subtitleLabel = UILabel()
subtitleLabel.text = "太好了您可以使用sos了"
subtitleLabel.textColor = UIColor(hexStr: "#273246")
subtitleLabel.font = .systemFont(ofSize: 16, weight: .regular)
subtitleLabel.textAlignment = .center
contentView.addSubview(subtitleLabel)
subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
let doneButton = UIButton(type: .custom)
doneButton.setTitle("我知道了", for: .normal)
doneButton.setTitleColor(.white, for: .normal)
doneButton.titleLabel?.font = FontManager.boboBold(18)
doneButton.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
doneButton.cornerRadius = 20
doneButton.addTarget(self, action: #selector(finishPractice), for: .touchUpInside)
contentView.addSubview(doneButton)
doneButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
modeBadge.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 28),
modeBadge.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
modeBadge.widthAnchor.constraint(equalToConstant: 139),
modeBadge.heightAnchor.constraint(equalToConstant: 40),
imageView.topAnchor.constraint(equalTo: modeBadge.bottomAnchor, constant: 72),
imageView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
imageView.widthAnchor.constraint(equalToConstant: 270),
imageView.heightAnchor.constraint(equalToConstant: 190),
titleLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 52),
titleLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 34),
subtitleLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
doneButton.topAnchor.constraint(greaterThanOrEqualTo: subtitleLabel.bottomAnchor, constant: 30),
doneButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 30),
doneButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -30),
doneButton.heightAnchor.constraint(equalToConstant: 56),
doneButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -(kSafeBottomMargin + 20))
])
}
private func makeVerticalContentView(in container: UIView) -> UIView {
let scrollView = UIScrollView()
scrollView.showsVerticalScrollIndicator = false
scrollView.alwaysBounceVertical = false
container.addSubview(scrollView)
scrollView.translatesAutoresizingMaskIntoConstraints = false
let contentView = UIView()
scrollView.addSubview(contentView)
contentView.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),
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),
contentView.heightAnchor.constraint(greaterThanOrEqualTo: scrollView.frameLayoutGuide.heightAnchor)
])
return contentView
}
private func setupActions() {
practicePulseView.onTap = { [weak self] in
guard let self, self.state == .practiceIdle else { return }
self.setState(.practiceCountdown)
}
practiceCountdownRing.onCompleted = { [weak self] in
guard let self, self.state == .practiceCountdown else { return }
self.setState(.success)
}
practiceSlider.onCompleted = { [weak self] in
guard let self, self.state == .practiceCountdown else { return }
self.practiceCountdownRing.stop()
self.setState(.practiceIdle)
}
}
private func setState(_ state: State) {
self.state = state
guideView.isHidden = state != .guide
practiceIdleView.isHidden = state != .practiceIdle
practiceCountdownView.isHidden = state != .practiceCountdown
successView.isHidden = state != .success
switch state {
case .guide:
practiceCountdownRing.stop()
practicePulseView.setAnimationEnabled(false)
case .practiceIdle:
practiceCountdownRing.stop()
practiceSlider.configure(style: .practiceCountdown)
practiceSlider.setInteractionEnabled(true)
practicePulseView.setShowsHand(true)
practicePulseView.setAnimationEnabled(true)
case .practiceCountdown:
practicePulseView.setAnimationEnabled(false)
practiceSlider.configure(style: .practiceCountdown)
practiceSlider.setInteractionEnabled(true)
practiceCountdownRing.start(duration: 10)
case .success:
practiceCountdownRing.stop(reset: false)
practicePulseView.setAnimationEnabled(false)
}
}
@objc private func startPractice() {
setState(.practiceIdle)
}
@objc private func finishPractice() {
var completedUsers = Defaults[\.sosIsPracticeList]
if !completedUsers.contains(AppContextManager.shared.userId) {
completedUsers.append(AppContextManager.shared.userId)
Defaults[\.sosIsPracticeList] = completedUsers
}
UIView.animate(withDuration: 0.25, animations: {
self.alpha = 0
}, completion: { _ in
self.isHidden = true
self.alpha = 1
})
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
guard scrollView === guideScrollView, scrollView.bounds.width > 0 else { return }
pageControl.currentPage = Int(round(scrollView.contentOffset.x / scrollView.bounds.width))
}
private static var cancelTitle: NSAttributedString {
let title = NSMutableAttributedString(
string: "滑动取消",
attributes: [.foregroundColor: UIColor(hexStr: "#333333")]
)
title.addAttribute(
.foregroundColor,
value: UIColor(hexStr: "#FF383C"),
range: NSRange(location: 2, length: 2)
)
return title
}
}
private final class SOSPracticeModeBadge: UIView {
private let gradientLayer = CAGradientLayer()
override init(frame: CGRect) {
super.init(frame: frame)
gradientLayer.colors = [
UIColor(hexStr: "#DDF4FF").cgColor,
UIColor(hexStr: "#52C5F4").cgColor,
UIColor(hexStr: "#DDF4FF").cgColor
]
gradientLayer.startPoint = CGPoint(x: 0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1, y: 0.5)
layer.addSublayer(gradientLayer)
let label = UILabel()
label.text = "练习模式"
label.textColor = .white
label.font = .systemFont(ofSize: 16, weight: .semibold)
label.textAlignment = .center
addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
label.topAnchor.constraint(equalTo: topAnchor),
label.leadingAnchor.constraint(equalTo: leadingAnchor),
label.trailingAnchor.constraint(equalTo: trailingAnchor),
label.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
gradientLayer.frame = bounds
}
}