简单二维码扫描实现

1,174 阅读6分钟
原文链接: bignerdcoding.com

最早熟识二维码还是引文微信扫一扫功能。其实二维码最早的设计初衷是用于追踪工业生产中的产品并替换信息存储能力有限的条形码。不过这些年智能手机的快速普及让二维码的应用场景得到了极大的拓展,所以作为一名开发者或迟或早都将要面对二维码识别问题。在很久以前的 iOS 开发中这个功能不得不依靠第三方库来实现,后来 Apple 在 AVFoundation 中极大的丰富了这类条码的识别能力。下面我将用很少的代码实现该功能,并且介绍 AVFoundation 中媒体捕捉的基本概念。

创建 Demo

Demo 的功能和 UI 都非常简单:我们通过摄像头对二维码进行扫描然后对获得信息进行解码并跳转到对应的 URL 网页,在扫描的过程中我们还会对其中的二维码外框进行高亮标记。其实二维码识别功能的实现非常的简单和直接,所有这些条码识别包括二维码其实都是基于 AVFoundation 框架的媒体捕捉。媒体捕捉的简单结构图如下,具体概念会在后面提到,当然还有一部分与输出信息处理的代理没有在图中列出来可以自己查看官方文档。

AVCapture

导入 AVFoundation 框架

首先我们新建一个视图控制器 QRScannerController.swift,然后在文件的头部导入框架:

import AVFoundation

然后我们在 QRScannerController.swift 文件头部新建如下的变量:

var captureSession: AVCaptureSession?
var videoPreviewLayer: AVCaptureVideoPreviewLayer?
var qrCodeFrameView: UIView?

并对 QRScannerController 进行拓展实现 AVCaptureMetadataOutputObjectsDelegate 代理(用于二维码解码,详情看后面):

extension QRScannerController:AVCaptureMetadataOutputObjectsDelegate {

}

初始化媒体捕捉环境

正如前面提到的二维码的识别都是基于 AVFoundation 框架中的媒体捕捉功能,所以最重要当然是上面结构图中核心:AVCaptureSession 所代表的捕捉环境的设置了。从结构图中我们能清晰的看见,AVCaptureSession 首先需要就是输入设备,也就是一个 AVCaptureDeviceInput 的实例(可能为摄像头、麦克风),所以我们在 viewDidLoad 中加入以下代码:

let captureDevice = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeVideo)

do {     
    //初始化媒体捕捉的输入流
    let input = try AVCaptureDeviceInput(device: captureDevice)

    //初始化captureSession
    captureSession = AVCaptureSession()

    //设置输入到Session
    captureSession?.addInput(input)

}  catch  {
    // 捕获到移除就退出
    print(error)
    return
}

因为我们的目标是实现二维码识别,所以这里调用了 defaultDevice(withMediaType:) 方法,并使用 AVMediaTypeVideo 为参数创建了视频设备的实例(在 iOS 中返回的是默认的后摄像头,而 macOS 中返回的是内置的 FaceTime 摄像头)。

为了实现实时捕捉,我们将上面设置好的输入设备实例添加到了 AVCaptureSession 实例中。AVCaptureSession 作为 AVFoundation 捕捉功能的核心类,其实它的功能类似于一个虚拟的 “插线板”,用来连接各种输入、输出的资源。AVCaptureSession 会从物理设备中获得数据流并将这些数据输出到多个目的地,而开发人员可以按照要需求对这些线路进行动态配置。

接下来就需要设置输出对象了,在框架中二维码对应的数据输出类型为 AVCaptureMetaDataOutput。该类对数据的处理都是通过代理 AVCaptureMetadataOutputObjectsDelegate 来实现的,这也是为什么上面我会对 QRScannerController 控制器进行拓展的原因。我们在 viewDidLoad do 代码块中加入如下代码:

//设置输入流
let captureMetadataOutput = AVCaptureMetadataOutput()
captureSession?.addOutput(captureMetadataOutput)

//设置代理并指定输出为二维码
captureMetadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
captureMetadataOutput.metadataObjectTypes = [AVMetadataObjectTypeQRCode]

上面的代码中我们将委托设置到默认的串行队列(Apple 文档中要求为串行队列),等待委托对获取的元数据进行下一步处理。同时我们将 metadataObjectTypes 属性设为了 AVMetadataObjectTypeQRCode 也就是二维码,其他的类型有:

  • UPC-E (AVMetadataObjectTypeUPCECode)
  • 2Code 39 (AVMetadataObjectTypeCode39Code)
  • Code 39 mod 43 (AVMetadataObjectTypeCode39Mod43Code)
  • Code 93 (AVMetadataObjectTypeCode93Code)
  • Code 128 (AVMetadataObjectTypeCode128Code)
  • EAN-8 (AVMetadataObjectTypeEAN8Code)
  • EAN-13 (AVMetadataObjectTypeEAN13Code)
  • Aztec (AVMetadataObjectTypeAztecCode)
  • PDF417 (AVMetadataObjectTypePDF417Code)

设置好输入、输出后,我们还缺了个非常重要的功能,那就是实时预览抓捕的场景。幸运的是,框架中已经自带了 AVCaptureVideoPreviewLayer 类来满足该需求,该类是 Core Animation 中 CALayer 的一个 SubClass。设置的代码如下:

//捕捉的实时预览图
videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
videoPreviewLayer?.videoGravity = AVLayerVideoGravityResizeAspectFill
videoPreviewLayer?.frame = view.layer.bounds
view.layer.addSublayer(videoPreviewLayer!)

万事具备后,我们开始捕捉:

// 开始捕获
captureSession?.startRunning()

如果你现在就在设备上运行 Demo 的话,对不起 Apple 会让你 Crash😂。iOS10 之后 Apple 的隐私策略更紧了,我们需要在 Info.plist 中配置摄像头的访问权限。配置好之后的运行图如下:

IMG_0803

完善二维码的识别

现在 Demo 已经可以检测二维码了,但是我们最终的目的是能够识别其中的信息并将其解码。所以接下来我们实现下面两个功能,进一步完善我们的 Demo:

  • 对二维码的外框进行高亮显示
  • 识别二维码中的 URL 信息并实现跳转。

为了实现对二维码区域进行高亮显示,我们先要在 viewDidLoad do 代码块中加入如下代码:

//初始化高亮图
qrCodeFrameView = UIView()
if let qrCodeFrameView = qrCodeFrameView {
    qrCodeFrameView.layer.borderColor = UIColor.green.cgColor
    qrCodeFrameView.layer.borderWidth = 2
    view.addSubview(qrCodeFrameView)
    view.bringSubview(toFront: qrCodeFrameView)
}

上面的代码中 qrCodeFrameView 被初始化为了一个边框为绿色、边框宽度为 2 的 UIView 对象,并且默认视图大小为 zero,后面在处理捕获对象的时候再动态的改变视图的大小。

前面已经提到过,当 AVCaptureMetadataOutput 识别了二维码对象之后会将对象转交给代理 AVCaptureMetadataOutputObjectsDelegate 来做进一步处理。所以下面我们需要在 extension 实现代理中的方法,代码如下:

func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [Any]!, from connection: AVCaptureConnection!) {

    //检查是否正确捕获到对象
    if metadataObjects == nil || metadataObjects.count == 0 {
        //设置二维码边框为空
        qrCodeFrameView?.frame = CGRect.zero
        return
    }

    // 取出第一个对象
    let metadataObj = metadataObjects[0] as! AVMetadataMachineReadableCodeObject

    if metadataObj.type == AVMetadataObjectTypeQRCode {
        //绿色高亮二维码区域
        let barCodeObject = videoPreviewLayer?.transformedMetadataObject(for: metadataObj)
        qrCodeFrameView?.frame = barCodeObject!.bounds

        if metadataObj.stringValue != nil {
            captureSession?.stopRunning()
            let url = metadataObj.stringValue!
            let safari = SFSafariViewController.init(url: URL.init(string:url)!);

            self.navigationController?.pushViewController(safari, animated: true);
        }
    }
}

该方法中的参数 metadataObjects 是一个数组对象,其中包含了所有输出的捕获信息。所以首先第一件事就是在数组为 nil 或者不含任何信息的时候将高亮区域重制为 zero。当数组中包含输出信息的时候,我们取出第一个并且验证类型是否为我们需要的二维码类型。然后我们设置高亮区域并取出其中包含的 URL 信息。最后我们跳转到该 URL 所在的网址并且停止扫描动作。效果如下图:

QRCodeRead

总结

伴随着 iOS 系统的更新迭代,Apple 官方提供的框架也越来越丰富功能越越来越强。除了那些网络等基本模块类库外,我一向反对在项目中使用太多的第三方库。因为我可能只是需要其中很小的一部分功能,而这些类库的功能往往都是大而全而且类库之间也有很多重复的代码。我们应该努力去挖掘系统框架的能力,尽量少的引入第三方轮子,要知道每多导入一个三方库就给系统增加了一个变量。轮子用的多了人会产生一种思维惰性,一切都指望着 Github。Less is More!

参考文章