iOS(Swift) 二维码扫描

实现相机扫描二维码, 相册选取图片识别二维码

 

  一. 导入 framework

import AVFoundation//二维码扫描
import CoreImage//二维码识别
import AudioToolbox//系统音效

二. 服从协议

AVCaptureMetadataOutputObjectsDelegate//扫描二维码
CALayerDelegate// CALayer 绘制
UINavigationControllerDelegate//图片选择控制器
UIImagePickerControllerDelegate

三.代码实现

//调用摄像头
    func setUpCamera() {
        //创建 device
        guard let cameraDevice = AVCaptureDevice.default(for: .video) else {
            print("不支持摄像头")
            return
        }
        
        //创建输入, 输出流
        let deviceInput: AVCaptureInput
        do{
            deviceInput = try AVCaptureDeviceInput(device: cameraDevice)
        }catch {
            print("不支持摄像头")
            return
        }
        let deviceOutput = AVCaptureMetadataOutput()
        deviceOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
        
        //session 设置
        let session = AVCaptureSession()
    
        //采集质量
        session.canSetSessionPreset(AVCaptureSession.Preset.high)
        //添加输入, 输出流
        if session.canAddInput(deviceInput) {
            session.addInput(deviceInput)
        }
        if session.canAddOutput(deviceOutput) {
            session.addOutput(deviceOutput)
        }
        
        //条码类型 AVMetadataObjectTypeQRCode, 设置支持的扫码所支持的格式
        deviceOutput.metadataObjectTypes = [AVMetadataObject.ObjectType.ean13,
                                       AVMetadataObject.ObjectType.ean8,
                                       AVMetadataObject.ObjectType.code128,
                                       AVMetadataObject.ObjectType.qr]
        
        //设置预览图层
        let previewLayer = AVCaptureVideoPreviewLayer(session: session)
        previewLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
        previewLayer.frame = view.bounds
        view.layer.insertSublayer(previewLayer, at: 0)
        
        //设置扫描区域, 也可以自己写方法计算
        NotificationCenter.default.addObserver(forName: NSNotification.Name.AVCaptureInputPortFormatDescriptionDidChange, object: nil, queue: nil) { [weak self](noti) in
            guard let strongSelf = self else { return }
            deviceOutput.rectOfInterest = previewLayer.metadataOutputRectConverted(fromLayerRect: strongSelf.scanFrameView.frame)
        }
        
        //蒙板
        let shadowLayer = CALayer()
        shadowLayer.frame = view.bounds
        shadowLayer.delegate = self
        view.layer.insertSublayer(shadowLayer, above: previewLayer)
        shadowLayer.setNeedsDisplay()
        
        self.maskLayer = shadowLayer
        self.session = session
    }
    
    //MARK: CALayerDelegate, 创建蒙板
    func draw(_ layer: CALayer, in ctx: CGContext) {
        if layer == maskLayer {
            UIGraphicsBeginImageContextWithOptions((maskLayer?.frame.size)!, false, 1)
            //蒙板颜色
            ctx.setFillColor(UIColor.RGBColor(r: 47, g: 47, b: 47, alpha: 0.6).cgColor)
            ctx.fill((maskLayer?.frame)!)
            let scanFrame = view.convert(scanFrameView.frame, from: scanFrameView.superview)
            //空出中间一块
            ctx.clear(scanFrame)
        }
    }
    
    //扫描线移动
    func scanAction() {
        let startPoint = CGPoint(x: scanline.center.x, y: scanFrameView.frame.minY)
        let endPoint = CGPoint(x: scanline.center.x, y: scanFrameView.frame.maxY)
        let basicAnimation = CABasicAnimation(keyPath: "position")
        basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
        basicAnimation.fromValue = NSValue(cgPoint: startPoint)
        basicAnimation.toValue = NSValue(cgPoint: endPoint)
        basicAnimation.duration = 4
        basicAnimation.repeatCount = MAXFLOAT
        basicAnimation.autoreverses = false
        scanline.layer.add(basicAnimation, forKey: nil)
    }

注意:

1. 扫描区域不设置的时候, 默认为整个屏幕;

自定义大小时设置  AVCaptureMetadataOutput 的 rectOfInterest 属性, rectOfInterest 为 CGRect 类型, 但是它的四个值和传统的不一样,是(y,x,高,宽)且是比例值,取值范围为0~1。

设置方法有两种: (1)  自定义方法计算 (2) 系统方法计算, 此时要放在通知里, 否则不起作用.记得在 deinit 方法中移除观察者

//自定义方法计算扫描框尺寸

    func rectOfInterestByScanViewRect(rect:CGRect) -> CGRect{

        let width = self.view.frame.size.width

        let height = self.view.frame.size.height

        let x = rect.minY / height

        let y = rect.minX / width

        let w = rect.size.height / height

        let h = rect.size.width / width

        return CGRect(x: x, y: y, width: w, height: h)

    }
//系统方法实现 
NotificationCenter.default.addObserver(forName: NSNotification.Name.AVCaptureInputPortFormatDescriptionDidChange, object: nil, queue: nil) { [weak self](noti) in
            guard let strongSelf = self else { return }
            deviceOutput.rectOfInterest = previewLayer.metadataOutputRectConverted(fromLayerRect: strongSelf.scanFrameView.frame)
        }

2. 创建扫描区域周围的蒙板时, 一种方法是在扫描框周围添加 UIView, 也可以像上文中创建一个 layer, 然后去掉中间扫描框的范围.

使用  layer 时要注意服从 CALayerDelegate 协议, 设置代理, 最后  setNeedsDisplay(),  不写  setNeedsDisplay() 不会执行.

在deinit 方法中把 delegate 置为 nil, 否则可能 crash

deinit {
        maskLayer?.delegate = nil
        NotificationCenter.default.removeObserver(self)
        print("deinit ~ \(self)")
    }

相机识别

// MARK: - AVCaptureMetadataOutputObjectsDelegate
    //捕获条码代理协议
    func metadataOutput(_ captureOutput: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
        if metadataObjects.count > 0 {
            playSystemSound()
            //停止扫描
            session?.stopRunning()
            //取识别到的第一个二维码
            let metadataobject = metadataObjects.first as? AVMetadataMachineReadableCodeObject
            //二维码的值
            stringValue = metadataobject?.stringValue

           //todoSomething.....
        }
    }

相册选取照片识别

//MARK: UIImagePickerControllerDelegate
    //打开本地相册
    func openLocalPhoto() {
        let imagePicker = UIImagePickerController()
        imagePicker.sourceType = .photoLibrary
        imagePicker.delegate = self
        self.present(imagePicker, animated: true, completion: nil)
    }
    
    //选择完成
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        //选择的类型是照片
        let type = info[UIImagePickerController.InfoKey.mediaType] as! String
        if type == "public.image" {
            //获取照片
            pickedImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage
            print(pickedImage.size)
            picker.dismiss(animated: true) {
                self.scanQRCodeFromPhotoLibrary(image: self.pickedImage)
            }
        }
    }
    
    //取消选择
    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        picker.dismiss(animated: true, completion: nil)
    }
    
    //识别二维码
    @objc func scanQRCodeFromPhotoLibrary(image: UIImage) {
        guard let cgImage = image.cgImage else { return }
        /// 这里设置了识别的精准程度为High,不过这个可能会有一点耗时。
        if let detector = CIDetector(ofType: CIDetectorTypeQRCode, context: nil, options: [CIDetectorAccuracy: CIDetectorAccuracyHigh]) {
            let features = detector.features(in: CIImage(cgImage: cgImage))
            for feature in features { // 这里实际上可以识别两张二维码,在这里只取第一张(左边或者上边)
                if let qrFeature = feature as? CIQRCodeFeature {
                    playSystemSound()
                    session?.stopRunning()
                    //获取识别出的字符串
                    stringValue = qrFeature.messageString
                   
                    //to do something.....

                    return
                }
            }
        }else {
            //没有识别到二维码

            //to do something...
        }
        
    }
    
    func playSystemSound() {
        DispatchQueue.global().async {
            //播放音效
            let url = Bundle.main.url(forResource: "scanSuccess.wav", withExtension: nil)
            var soundID: SystemSoundID = 8787
            AudioServicesCreateSystemSoundID(url! as CFURL, &soundID)
            AudioServicesPlaySystemSound(soundID)
        }
    }

 

你可能感兴趣的:(iOS)