// // SOSPracticeView.swift // QuickLocation // // Created by 八条 on 2026/6/18. // import UIKit import RxSwift import RxCocoa import RxGesture import Lottie import SwiftyUserDefaults class SOSPracticeView: UIView { var disposeBag = DisposeBag() var countDownFinish: Bool = false private func setupRx() { exclamationLottieView.rx.tapGesture.subscribe(onNext: { _ in UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseInOut], animations: { self.countDownView.alpha = 1 }, completion: { _ in self.countDownLottieView.play { completed in guard completed else { return } if let path = Bundle.main.path(forResource: "red-exclamation", ofType: "json") { self.countDownLottieView.animation = LottieAnimation.filepath(path) self.countDownLottieView.loopMode = .loop self.countDownLottieView.play() self.countDownTipsLab.text = "已将你的SOS和位置发送到你的圈子和紧急联系人。" self.countDownFinish = true } } }) }).disposed(by: disposeBag) sliderIcon.rx.panGesture() .subscribe(onNext: { [weak self] gesture in guard let self = self else { return } let translation = gesture.translation(in: self.sliderView) let maxX = self.sliderView.bounds.width - self.sliderIcon.frame.width switch gesture.state { case .began, .changed: var newX = self.sliderIcon.frame.minX + translation.x newX = max(0, min(maxX, newX)) self.sliderIcon.frame.origin.x = newX gesture.setTranslation(.zero, in: self.sliderView) case .ended: if self.sliderIcon.frame.minX >= maxX - 5 { if countDownFinish { UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseInOut]) { self.finishView.alpha = 1 } } else { self.countDownView.alpha = 0 self.countDownLottieView.stop() } } UIView.animate(withDuration: 0.3) { self.sliderIcon.frame.origin.x = 3 } default: break } }) .disposed(by: disposeBag) } private func setupUI() { addSubview(titleView) addSubview(exclamationLottieView) addSubview(tipsLab) addSubview(bottomTipsLab) addSubview(scrollView) addSubview(pageControl) addSubview(countDownView) addSubview(finishView) titleView.layoutChain .top(15) .centerX() exclamationLottieView.layoutChain .topToBottomOfView(titleView, offset: 13) .edgesHorzontal(28) .heightToWidth(1) tipsLab.layoutChain .topToBottomOfView(exclamationLottieView, offset: 60) .centerX() bottomTipsLab.layoutChain .centerX() .bottom(kSafeBottomMargin + 40) scrollView.layoutChain .top(15) .edges(excludingEdge: .top) pageControl.layoutChain .centerX() .bottom(kSafeBottomMargin + 80) countDownView.layoutChain .edges() finishView.layoutChain.edges() } lazy var titleView: UIView = { let view = UIView() view.backgroundColor = .clear view.addSubview(titleLab) titleLab.layoutChain .edgesVertical(9) .edgesHorzontal(38) return view }() lazy var titleLab: UILabel = { let label = UILabel() label.text = "开始练习" label.font = .systemFont(ofSize: 16, weight: .medium) label.textColor = .white label.textAlignment = .center return label }() lazy var exclamationLottieView: LottieAnimationView = { let view = LottieAnimationView(name: "yellow-exclamation") view.loopMode = .loop return view }() lazy var tipsLab: UILabel = { let label = UILabel() label.text = "当您感到紧张或不安全时,请按住此按钮。" label.font = .systemFont(ofSize: 16, weight: .medium) label.textColor = UIColor(hexStr: "#333333") return label }() lazy var bottomTipsLab: UILabel = { let label = UILabel() label.textColor = UIColor(hexStr: "#333333") let text = "轻点感叹号发送SOS" let attr = NSMutableAttributedString(string: text) attr.addAttribute(.font, value: UIFont.systemFont(ofSize: 20, weight: .semibold), range: NSRange(location: 0, length: text.count)) attr.addAttribute(.foregroundColor, value: UIColor(hexStr: "#FF383C"), range: NSRange(location: "轻点感叹号发送".count, length: "SOS".count)) label.attributedText = attr return label }() lazy var scrollView: UIScrollView = { let view = UIScrollView() view.backgroundColor = .white view.isPagingEnabled = true view.showsHorizontalScrollIndicator = false view.bounces = false view.delegate = self view.addSubview(scrollContentView) scrollContentView.layoutChain.edges().heightToView(view) let view1 = UIView() view1.backgroundColor = .clear let img1 = UIImageView(image: UIImage(named: "SOS/1")) let nextBtn = UIButton(type: .custom) nextBtn.setTitle("开始设置", for: .normal) nextBtn.setTitleColor(.white, for: .normal) nextBtn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) nextBtn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal) nextBtn.cornerRadius = 25 nextBtn.rx.tap.subscribe(onNext: { _ in view.setContentOffset(CGPointMake(kScreenWidth, 0), animated: true) self.pageControl.currentPage = 1 }).disposed(by: disposeBag) view1.addSubview(img1) view1.addSubview(nextBtn) scrollContentView.addSubview(view1) view1.layoutChain.edges(excludingEdge: .right).width(kScreenWidth) img1.layoutChain .top() .edgesHorzontal(39) .heightToWidth(471/297) nextBtn.layoutChain .height(50) .edgesHorzontal(30) .bottom(kSafeBottomMargin + 20) let view2 = UIView() view2.backgroundColor = .clear let img2 = UIImageView(image: UIImage(named: "SOS/2")) view2.addSubview(img2) scrollContentView.addSubview(view2) view2.layoutChain.top().bottom().leftToRightOfView(view1).widthToView(view1) img2.layoutChain .top() .edgesHorzontal(39) .heightToWidth(471/297) let view3 = UIView() view3.backgroundColor = .clear let img3 = UIImageView(image: UIImage(named: "SOS/3")) let doneBtn = UIButton(type: .custom) doneBtn.setTitle("练习SOS触发", for: .normal) doneBtn.setTitleColor(.white, for: .normal) doneBtn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) doneBtn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal) doneBtn.cornerRadius = 25 doneBtn.rx.tap.subscribe(onNext: { _ in self.scrollView.isHidden = true self.pageControl.isHidden = true }).disposed(by: disposeBag) view3.addSubview(img3) view3.addSubview(doneBtn) scrollContentView.addSubview(view3) view3.layoutChain.top().bottom().right().leftToRightOfView(view2).widthToView(view1) img3.layoutChain .top() .edgesHorzontal(39) .heightToWidth(471/297) doneBtn.layoutChain .height(50) .edgesHorzontal(30) .bottom(kSafeBottomMargin + 20) return view }() lazy var scrollContentView: UIView = { let view = UIView() view.backgroundColor = .clear return view }() lazy var pageControl: UIPageControl = { let pc = UIPageControl() pc.numberOfPages = 3 pc.currentPageIndicatorTintColor = UIColor(hexStr: "#57C7FF") pc.pageIndicatorTintColor = UIColor(hexStr: "#D9D9D9", alpha: 1) return pc }() // 倒计时 lazy var countDownView: UIView = { let view = UIView() view.backgroundColor = .white view.alpha = 0 view.addSubview(countDownTitleView) countDownTitleView.layoutChain .top(15) .centerX() view.addSubview(countDownLottieView) countDownLottieView.layoutChain .topToBottomOfView(countDownTitleView, offset: 13) .edgesHorzontal(27) .heightToWidth(1) let titleLab = UILabel() titleLab.text = "滑动取消" titleLab.font = .systemFont(ofSize: 26, weight: .semibold) view.addSubview(titleLab) titleLab.layoutChain .topToBottomOfView(countDownLottieView, offset: 15) .centerX() view.addSubview(countDownTipsLab) countDownTipsLab.layoutChain .topToBottomOfView(titleLab, offset: 8) .edgesHorzontal(38) view.addSubview(sliderView) sliderView.layoutChain .edgesHorzontal(30) .bottom(kSafeBottomMargin + 30) .height(50) return view }() lazy var countDownTitleView: UIView = { let view = UIView() view.backgroundColor = .clear view.addSubview(countDownTitleLab) countDownTitleLab.layoutChain .edgesVertical(9) .edgesHorzontal(38) return view }() lazy var countDownTitleLab: UILabel = { let label = UILabel() label.text = "练习模式" label.font = .systemFont(ofSize: 16, weight: .medium) label.textColor = .white label.textAlignment = .center return label }() lazy var countDownLottieView: LottieAnimationView = { let view = LottieAnimationView(name: "10-second-timer") view.loopMode = .playOnce return view }() lazy var countDownTipsLab: UILabel = { let tipsLab = UILabel() tipsLab.text = "10秒后,会将你的SOS和位置发送到你的圈子和紧急联系人。" tipsLab.font = .systemFont(ofSize: 16, weight: .medium) tipsLab.textAlignment = .center tipsLab.numberOfLines = 0 return tipsLab }() lazy var sliderView: UIView = { let view = UIView() view.backgroundColor = .clear view.cornerRadius = 25 let imgView = UIImageView(image: UIImage(named: "Common/button_bg_2")) imgView.contentMode = .scaleAspectFill view.addSubview(imgView) imgView.layoutChain.edges() let label = UILabel() label.text = "滑动以取消SOS" label.textColor = .white label.font = .systemFont(ofSize: 16, weight: .medium) view.addSubview(label) label.layoutChain.centerX().centerY() view.addSubview(sliderIcon) return view }() lazy var sliderIcon: UIImageView = { let view = UIImageView(frame: CGRectMake(3, 2, 46, 46)) view.image = UIImage(named: "SOS/slider") view.isUserInteractionEnabled = true return view }() lazy var finishView: UIView = { let view = UIView() view.backgroundColor = .white view.alpha = 0 let imgView = UIImageView(image: UIImage(named: "SOS/finish")) imgView.contentMode = .scaleAspectFill view.addSubview(imgView) imgView.layoutChain .top(50) .edgesHorzontal(70) .heightToWidth(356/235) let doneBtn = UIButton(type: .custom) doneBtn.setTitle("收到了", for: .normal) doneBtn.setTitleColor(.white, for: .normal) doneBtn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) doneBtn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal) doneBtn.cornerRadius = 25 doneBtn.rx.tap.subscribe(onNext: { _ in var sosIsPracticeList = Defaults[\.sosIsPracticeList] sosIsPracticeList.append(AppContextManager.shared.userId) Defaults[\.sosIsPracticeList] = sosIsPracticeList UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseInOut], animations: { self.alpha = 0 }, completion: { _ in self.isHidden = true }) }).disposed(by: disposeBag) view.addSubview(doneBtn) doneBtn.layoutChain .height(50) .edgesHorzontal(30) .bottom(kSafeBottomMargin + 20) return view }() override init(frame: CGRect) { super.init(frame: .zero) self.isHidden = true backgroundColor = .white setupUI() setupRx() exclamationLottieView.play() } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func layoutSubviews() { super.layoutSubviews() self.layoutIfNeeded() titleView.setGradientLayer(frame: titleView.bounds, startPoint: CGPoint(x: 0, y: 0.5), endPoint: CGPoint(x: 1, y: 0.5), colors: [UIColor(hexStr: "#5CBBFF", alpha: 0), UIColor(hexStr: "#5CBBFF"), UIColor(hexStr: "#5CBBFF", alpha: 0)], locations: [0, 0.5, 1]) countDownView.layoutIfNeeded() countDownTitleView.setGradientLayer(frame: countDownTitleView.bounds, startPoint: CGPoint(x: 0, y: 0.5), endPoint: CGPoint(x: 1, y: 0.5), colors: [UIColor(hexStr: "#5CBBFF", alpha: 0), UIColor(hexStr: "#5CBBFF"), UIColor(hexStr: "#5CBBFF", alpha: 0)], locations: [0, 0.5, 1]) } } // MARK: - UIScrollViewDelegate (page control) extension SOSPracticeView: UIScrollViewDelegate { func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { guard scrollView == self.scrollView else { return } let page = Int(scrollView.contentOffset.x / scrollView.bounds.width) pageControl.currentPage = page } }