// // QRCodeScannerViewController.swift // suixinkan // import AVFoundation import SnapKit import UIKit /// AVFoundation 二维码扫描页。 final class QRCodeScannerViewController: BaseViewController, AVCaptureMetadataOutputObjectsDelegate { var onScanResult: ((String) -> Void)? private let captureSession = AVCaptureSession() private var previewLayer: AVCaptureVideoPreviewLayer? private var hasHandledResult = false private let maskView = UIView() private let hintLabel = UILabel() private let closeButton = UIButton(type: .system) override func setupNavigationBar() { navigationController?.setNavigationBarHidden(true, animated: false) } override func setupUI() { view.backgroundColor = .black hintLabel.text = "将二维码放入框内" hintLabel.textColor = .white hintLabel.font = .systemFont(ofSize: 14) hintLabel.textAlignment = .center closeButton.setTitle("关闭", for: .normal) closeButton.setTitleColor(.white, for: .normal) closeButton.addTarget(self, action: #selector(closeTapped), for: .touchUpInside) view.addSubview(maskView) view.addSubview(hintLabel) view.addSubview(closeButton) } override func setupConstraints() { closeButton.snp.makeConstraints { make in make.top.equalTo(view.safeAreaLayoutGuide).offset(12) make.trailing.equalToSuperview().inset(16) } hintLabel.snp.makeConstraints { make in make.centerX.equalToSuperview() make.bottom.equalTo(view.safeAreaLayoutGuide).inset(48) } maskView.snp.makeConstraints { make in make.center.equalToSuperview() make.size.equalTo(260) } } override func viewDidLoad() { super.viewDidLoad() checkCameraPermission() } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() previewLayer?.frame = view.bounds updateMask() } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) if captureSession.isRunning { captureSession.stopRunning() } } private func checkCameraPermission() { switch AVCaptureDevice.authorizationStatus(for: .video) { case .authorized: setupCamera() case .notDetermined: AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in DispatchQueue.main.async { granted ? self?.setupCamera() : self?.showPermissionDenied() } } default: showPermissionDenied() } } private func showPermissionDenied() { showToast("需要相机权限才能扫码") dismiss(animated: true) } private func setupCamera() { guard let device = AVCaptureDevice.default(for: .video), let input = try? AVCaptureDeviceInput(device: device) else { showToast("无法打开相机") return } guard captureSession.canAddInput(input) else { return } captureSession.addInput(input) let output = AVCaptureMetadataOutput() guard captureSession.canAddOutput(output) else { return } captureSession.addOutput(output) output.setMetadataObjectsDelegate(self, queue: DispatchQueue.main) output.metadataObjectTypes = [.qr] let layer = AVCaptureVideoPreviewLayer(session: captureSession) layer.videoGravity = .resizeAspectFill view.layer.insertSublayer(layer, at: 0) previewLayer = layer DispatchQueue.global(qos: .userInitiated).async { [weak self] in self?.captureSession.startRunning() } } private func updateMask() { maskView.layer.sublayers?.forEach { $0.removeFromSuperlayer() } let border = CAShapeLayer() border.path = UIBezierPath(roundedRect: maskView.bounds, cornerRadius: 12).cgPath border.strokeColor = AppColor.primary.cgColor border.fillColor = UIColor.clear.cgColor border.lineWidth = 2 maskView.layer.addSublayer(border) } func metadataOutput( _ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection ) { guard !hasHandledResult, let object = metadataObjects.first as? AVMetadataMachineReadableCodeObject, object.type == .qr, let value = object.stringValue, !value.isEmpty else { return } hasHandledResult = true captureSession.stopRunning() onScanResult?(value) dismiss(animated: true) } @objc private func closeTapped() { dismiss(animated: true) } }