// // ScanVC.swift // QuickLocation // // Created by 八条 on 2026/6/2. // import UIKit import AVFoundation import RxSwift class ScanVC: BaseViewController { override var isNavigationBarHidden: Bool { true } fileprivate var rootView: ScanView! override func loadView() { rootView = ScanView(frame: UIScreen.main.bounds) view = rootView } private var captureSession: AVCaptureSession! private var previewLayer: AVCaptureVideoPreviewLayer! private var isScanning = false override func viewDidLoad() { super.viewDidLoad() setupCamera() } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() previewLayer?.frame = view.bounds } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) if let session = captureSession, !session.isRunning { DispatchQueue.global(qos: .background).async { session.startRunning() } } startScanAnimation() } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) if let session = captureSession, session.isRunning { session.stopRunning() } rootView.scanLineView.layer.removeAllAnimations() } private func handleScanResult(text: String) { guard !isScanning else { return } isScanning = true captureSession.stopRunning() rootView.scanLineView.alpha = 0 rootView.scanLineView.layer.removeAllAnimations() requestOperateGroup(code: text) } // MARK: - API private func requestOperateGroup(code: String) { DLToast.showLoading() GroupService.operate(opType: "join", requestData: ["share_code" : code]).subscribe(onNext: { response in }, onError: { [weak self] (error) in DispatchQueue.main.asyncAfter(deadline: .now() + 3) { self?.isScanning = false self?.startSession() } }).disposed(by: disposeBag) } private func startSession() { if let session = captureSession, !session.isRunning { DispatchQueue.global(qos: .background).async { session.startRunning() } } startScanAnimation() } // MARK: - 扫描线动画 private func startScanAnimation() { rootView.scanLineView.layer.removeAllAnimations() rootView.scanLineView.alpha = 1 rootView.scanLineView.frame.origin.y = 0 let boxH = rootView.scanBoxView.bounds.height guard boxH > 0 else { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in self?.startScanAnimation() } return } UIView.animate(withDuration: 2.0, delay: 0, options: [.repeat, .curveLinear]) { self.rootView.scanLineView.frame.origin.y = boxH - 2 } } // MARK: - 相机配置 private func setupCamera() { let status = AVCaptureDevice.authorizationStatus(for: .video) switch status { case .authorized: break case .notDetermined: AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in DispatchQueue.main.async { if granted { self?.setupCamera() } } } return default: DLToast.show(text: "请在设置中开启相机权限") return } captureSession = AVCaptureSession() captureSession.sessionPreset = .high guard let videoDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else { DLToast.show(text: "未检测到摄像头") return } do { let input = try AVCaptureDeviceInput(device: videoDevice) if captureSession.canAddInput(input) { captureSession.addInput(input) } let output = AVCaptureMetadataOutput() if captureSession.canAddOutput(output) { captureSession.addOutput(output) output.setMetadataObjectsDelegate(self, queue: DispatchQueue.main) output.metadataObjectTypes = [.qr, .code128, .ean13, .ean8, .upce, .code39, .code39Mod43] } previewLayer = AVCaptureVideoPreviewLayer(session: captureSession) previewLayer.frame = view.bounds previewLayer.videoGravity = .resizeAspectFill view.layer.insertSublayer(previewLayer, at: 0) DispatchQueue.global(qos: .background).async { self.captureSession.startRunning() } } catch { DLToast.show(text: "相机初始化失败:\(error.localizedDescription)") } } } // MARK: - AVCaptureMetadataOutputObjectsDelegate extension ScanVC: AVCaptureMetadataOutputObjectsDelegate { func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { guard let obj = metadataObjects.first as? AVMetadataMachineReadableCodeObject, let result = obj.stringValue else { return } handleScanResult(text: result) } }