二維碼掃描控制器

import UIKit
import AVFoundation

/// 二維碼掃描控制器
class QRScanVC: UIViewController {
    // MARK: - 私有屬性

    /// 捕捉會(huì)話對(duì)象,管理輸入和輸出流
    private var captureSession: AVCaptureSession!

    /// 預(yù)覽圖層,用于顯示攝像頭畫面
    private var previewLayer: AVCaptureVideoPreviewLayer!

    /// 掃描線視圖
    private var scanningLine: UIView!

    /// 掃描線動(dòng)畫
    private var animation: CABasicAnimation!

    /// 掃描框的邊長(zhǎng)
    private let squareSize: CGFloat = 300

    /// 掃描框的 X 坐標(biāo)
    private var squareX: CGFloat = 0

    /// 掃描框的 Y 坐標(biāo)
    private var squareY: CGFloat = 0

    /// 掃描結(jié)果回調(diào)閉包
    private var scanResultBlock: ((String) -> Void)?

    // MARK: - 初始化

    /// 初始化控制器并傳入掃描結(jié)果回調(diào)
    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()             // 初始化相機(jī)
        addMaskToScannerView()         // 添加遮罩和掃描框
        setupCloseAndTitleView()       // 設(shè)置頂部關(guān)閉按鈕和標(biāo)題
        setupScanningLineAnimation()   // 設(shè)置掃描線動(dòng)畫
        setupPhotoLibraryButton()      // 添加相冊(cè)導(dǎo)入按鈕
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        scanningLine.layer.add(animation, forKey: "scanning") // 添加掃描動(dòng)畫
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        scanningLine.layer.removeAllAnimations() // 移除動(dòng)畫
    }
}

// MARK: - 相機(jī)相關(guān)設(shè)置
extension QRScanVC {
    /// 初始化相機(jī)掃描功能
    private func initializeCamera() {
        captureSession = AVCaptureSession()

        // 獲取默認(rèn)視頻設(shè)備(攝像頭)
        guard let videoDevice = AVCaptureDevice.default(for: .video),
              let videoInput = try? AVCaptureDeviceInput(device: videoDevice),
              captureSession.canAddInput(videoInput) else { return }

        // 添加視頻輸入流
        captureSession.addInput(videoInput)

        // 創(chuàng)建元數(shù)據(jù)輸出流并添加
        let metadataOutput = AVCaptureMetadataOutput()
        if captureSession.canAddOutput(metadataOutput) {
            captureSession.addOutput(metadataOutput)

            // 設(shè)置回調(diào)代理為 self,使用主線程回調(diào)
            metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
            metadataOutput.metadataObjectTypes = [.qr] // 只識(shí)別二維碼
        } else {
            return
        }

        // 創(chuàng)建預(yù)覽圖層,展示攝像頭畫面
        previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
        previewLayer.frame = view.bounds
        previewLayer.videoGravity = .resizeAspectFill
        view.layer.addSublayer(previewLayer)

        // 異步啟動(dòng)攝像頭掃描
        Asyncs.async { [weak self] in
            self?.captureSession.startRunning()
        }
    }

    /// 添加遮罩區(qū)域和中間的掃描框
    private func addMaskToScannerView() {
        squareX = (view.bounds.width - squareSize) / 2
        squareY = (view.bounds.height - squareSize) / 2

        // 創(chuàng)建遮罩視圖區(qū)域:上、下、左、右
        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))
        ]

        // 設(shè)置遮罩顏色并添加到視圖
        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)
    }

    /// 設(shè)置頂部關(guān)閉按鈕和標(biāo)題
    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()
        }
    }

    /// 掃描線動(dòng)畫配置
    private func setupScanningLineAnimation() {
        animation = CABasicAnimation(keyPath: "position.y")
        animation.fromValue = squareY
        animation.toValue = squareY + squareSize
        animation.duration = 2
        animation.autoreverses = true // 自動(dòng)反向運(yùn)動(dòng)
        animation.repeatCount = .infinity // 無限循環(huán)
    }
}

// MARK: - 掃描識(shí)別回調(diào)
extension QRScanVC: AVCaptureMetadataOutputObjectsDelegate {
    /// 當(dāng)掃描到二維碼結(jié)果后調(diào)用
    func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
        captureSession.stopRunning()
        if let metadata = metadataObjects.first as? AVMetadataMachineReadableCodeObject,
           let value = metadata.stringValue {
            // 掃描成功后震動(dòng)提示
            AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
            scanResultBlock?(value) // 回調(diào)結(jié)果
        }
        dismiss(animated: true)
    }
}

// MARK: - 相冊(cè)讀取二維碼
extension QRScanVC: TZImagePickerControllerDelegate {
    /// 添加“相冊(cè)導(dǎo)入二維碼”按鈕
    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~)
        }
    }

    /// 從相冊(cè)選擇圖片后,嘗試識(shí)別二維碼
    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: "識(shí)別二維碼內(nèi)容:\(message)")
            scanResultBlock?(message)
        }
        dismiss(animated: true)
    }
}

僅供參考

IMG_4059.PNG
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容