二维码扫描控制器

import UIKit
import AVFoundation

/// 二维码扫描控制器
class QRScanVC: UIViewController {
    // MARK: - 私有属性

    /// 捕捉会话对象,管理输入和输出流
    private var captureSession: AVCaptureSession!

    /// 预览图层,用于显示摄像头画面
    private var previewLayer: AVCaptureVideoPreviewLayer!

    /// 扫描线视图
    private var scanningLine: UIView!

    /// 扫描线动画
    private var animation: CABasicAnimation!

    /// 扫描框的边长
    private let squareSize: CGFloat = 300

    /// 扫描框的 X 坐标
    private var squareX: CGFloat = 0

    /// 扫描框的 Y 坐标
    private var squareY: CGFloat = 0

    /// 扫描结果回调闭包
    private var scanResultBlock: ((String) -> Void)?

    // MARK: - 初始化

    /// 初始化控制器并传入扫描结果回调
    convenience init(_ result: ((_ text: String) -> Void)?) {
        self.init(nibName: nil, bundle: nil)
        self.modalPresentationStyle = .fullScreen
        self.scanResultBlock = result
    }

    // MARK: - 生命周期

    override func viewDidLoad() {
        super.viewDidLoad()
        initializeCamera()             // 初始化相机
        addMaskToScannerView()         // 添加遮罩和扫描框
        setupCloseAndTitleView()       // 设置顶部关闭按钮和标题
        setupScanningLineAnimation()   // 设置扫描线动画
        setupPhotoLibraryButton()      // 添加相册导入按钮
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        scanningLine.layer.add(animation, forKey: "scanning") // 添加扫描动画
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        scanningLine.layer.removeAllAnimations() // 移除动画
    }
}

// MARK: - 相机相关设置
extension QRScanVC {
    /// 初始化相机扫描功能
    private func initializeCamera() {
        captureSession = AVCaptureSession()

        // 获取默认视频设备(摄像头)
        guard let videoDevice = AVCaptureDevice.default(for: .video),
              let videoInput = try? AVCaptureDeviceInput(device: videoDevice),
              captureSession.canAddInput(videoInput) else { return }

        // 添加视频输入流
        captureSession.addInput(videoInput)

        // 创建元数据输出流并添加
        let metadataOutput = AVCaptureMetadataOutput()
        if captureSession.canAddOutput(metadataOutput) {
            captureSession.addOutput(metadataOutput)

            // 设置回调代理为 self,使用主线程回调
            metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
            metadataOutput.metadataObjectTypes = [.qr] // 只识别二维码
        } else {
            return
        }

        // 创建预览图层,展示摄像头画面
        previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
        previewLayer.frame = view.bounds
        previewLayer.videoGravity = .resizeAspectFill
        view.layer.addSublayer(previewLayer)

        // 异步启动摄像头扫描
        Asyncs.async { [weak self] in
            self?.captureSession.startRunning()
        }
    }

    /// 添加遮罩区域和中间的扫描框
    private func addMaskToScannerView() {
        squareX = (view.bounds.width - squareSize) / 2
        squareY = (view.bounds.height - squareSize) / 2

        // 创建遮罩视图区域:上、下、左、右
        let masks = [
            UIView(frame: CGRect(x: 0, y: 0, width: view.bounds.width, height: squareY)),
            UIView(frame: CGRect(x: 0, y: squareY + squareSize, width: view.bounds.width, height: view.bounds.height - (squareY + squareSize))),
            UIView(frame: CGRect(x: 0, y: squareY, width: squareX, height: squareSize)),
            UIView(frame: CGRect(x: squareX + squareSize, y: squareY, width: view.bounds.width - (squareX + squareSize), height: squareSize))
        ]

        // 设置遮罩颜色并添加到视图
        masks.forEach {
            $0.backgroundColor = UIColor.black.withAlphaComponent(0.9)
            view.addSubview($0)
        }

        // 中间绿色边框扫描框
        let squareFrame = UIView(frame: CGRect(x: squareX, y: squareY, width: squareSize, height: squareSize))
        squareFrame.layer.borderColor = UIColor.green.cgColor
        squareFrame.layer.borderWidth = 3
        squareFrame.backgroundColor = .clear
        view.addSubview(squareFrame)

        // 添加红色扫描线
        scanningLine = UIView(frame: CGRect(x: squareX, y: squareY, width: squareSize, height: 2))
        scanningLine.backgroundColor = UIColor.red
        view.addSubview(scanningLine)
    }

    /// 设置顶部关闭按钮和标题
    private func setupCloseAndTitleView() {
        let closeButton = UIButton(image: "icon_close_w")
        closeButton.addActionWithBlock { [weak self] _ in
            self?.captureSession.stopRunning()
            self?.dismiss(animated: true)
        }
        view.addSubview(closeButton)
        closeButton.snp.makeConstraints { make in
            make.top.equalToSuperview().inset(ScreenStatusBarHeight + 2~)
            make.leading.equalToSuperview().inset(20~)
            make.size.equalTo(40~)
        }

        let titleLabel = UILabel(text: "Scan", size: 18, color: .cWhite, weight: .bold)
        view.addSubview(titleLabel)
        titleLabel.snp.makeConstraints { make in
            make.centerY.equalTo(closeButton)
            make.centerX.equalToSuperview()
        }
    }

    /// 扫描线动画配置
    private func setupScanningLineAnimation() {
        animation = CABasicAnimation(keyPath: "position.y")
        animation.fromValue = squareY
        animation.toValue = squareY + squareSize
        animation.duration = 2
        animation.autoreverses = true // 自动反向运动
        animation.repeatCount = .infinity // 无限循环
    }
}

// MARK: - 扫描识别回调
extension QRScanVC: AVCaptureMetadataOutputObjectsDelegate {
    /// 当扫描到二维码结果后调用
    func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
        captureSession.stopRunning()
        if let metadata = metadataObjects.first as? AVMetadataMachineReadableCodeObject,
           let value = metadata.stringValue {
            // 扫描成功后震动提示
            AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
            scanResultBlock?(value) // 回调结果
        }
        dismiss(animated: true)
    }
}

// MARK: - 相册读取二维码
extension QRScanVC: TZImagePickerControllerDelegate {
    /// 添加“相册导入二维码”按钮
    private func setupPhotoLibraryButton() {
        let photoButton = UIButton(image: "QRAlbum")
        photoButton.addActionWithBlock { [weak self] _ in
            guard let imagePicker = TZImagePickerController(maxImagesCount: 1, delegate: self) else { return }
            imagePicker.allowCrop = false
            imagePicker.allowTakeVideo = false
            imagePicker.allowPickingVideo = false
            imagePicker.preferredLanguage = LM.shared.currentLanguage.localeIdentifier
            imagePicker.modalPresentationStyle = .fullScreen
            self?.present(imagePicker, animated: true)
        }
        photoButton.cornerRadius(20~)
        view.addSubview(photoButton)
        photoButton.snp.makeConstraints { make in
            make.bottom.equalToSuperview().inset(ScreenBottomSafeAreaHeight + 20~)
            make.centerX.equalToSuperview()
            make.size.equalTo(40~)
        }
    }

    /// 从相册选择图片后,尝试识别二维码
    func imagePickerController(_ picker: TZImagePickerController!, didFinishPickingPhotos photos: [UIImage]!, sourceAssets: [Any]!, isSelectOriginalPhoto: Bool) {
        guard let image = photos.first else { return }
        captureSession.stopRunning()

        let ciImage = CIImage(image: image)!
        let context = CIContext()
        let detector = CIDetector(ofType: CIDetectorTypeQRCode, context: context, options: [CIDetectorAccuracy: CIDetectorAccuracyHigh])

        if let features = detector?.features(in: ciImage),
           let qrFeature = (features as? [CIQRCodeFeature])?.first,
           let message = qrFeature.messageString {
            PrintLog(message: "识别二维码内容:\(message)")
            scanResultBlock?(message)
        }
        dismiss(animated: true)
    }
}

仅供参考

IMG_4059.PNG
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容