• 作者:SIMON NG


什么是二维码?我相信大多数人都知道二维码是什么。即使你没有听说过二维码,但是看看上面的图片,你会恍然大悟,这就是二维码!

QR(Quick Response 的缩写)码是由Denso开发的一种二维条形码。二维码最初是为了跟踪零部件制造,近几年来,二维码作为一种编码着登录信息或者营销信息链接的识别码普及到了消费领域。与大众熟悉的条形码不同,二维码的信息包含在水平和垂直两个方向。这也有助于二维码以数字和字母的形式存储大量的数据。我并不想在这里谈论二维码的技术细节,如果你感兴趣可以去查阅二维码的官方网站。

随着iPhone和安卓手机的盛行,二维码的使用大幅度增加。在一些国家,二维码的踪迹随处可见。它们出现在杂志、报纸、广告、广告牌、名片,甚至食物菜单上。作为一个iOS开发者,你可能会想知道怎样让你的app读取二维码。在iOS7之前,你不得不依赖第三方库来实现扫描功能。现在,你可以使用内置的AVFoundation框架来发现和实时读取条形码。

创建一个扫描和翻译的二维码的app从未如此加简单。

创建一个二维码识别App

我们要创建的演示app非常简单和直接明了。在创建前,要非常清楚的知道,在iOS中任何的条形码扫描,包括二维码扫描,都是基于视频捕捉的。这也是为什么条形码扫描功能添加在AVFoundation框架之中。将这条铭记于心,它会帮助你理解整篇文章。
那么,demo app是怎样工作的呢?

下边的截图展示了APP的UI。这个应用相当于一个没有记录功能的视频捕捉应用。当应用程序启动时,它利用iPhone的后置摄像头自动识别二维码。被解码的信息(例如一个网址)显示在屏幕底部的右方。


就是这样简单。

要建立应用程序,你可以从这里下载项目模板。我已经预先创建了storyboard并且连接了message label。主屏幕用的是QRCodeViewController类,而屏幕扫描页面用的是QRScannerController类。


启动应用后,你可以点击扫描按钮来扫描视图。然后就会弹出二维码扫描的视图控制器页面。

理解应用的工作原理后,我们将着手开发应用的二维码扫描功能。

导入AVFoundation框架

我已经在项目模板中创建了app的用户界面。UI中的label是用于显示被解码的二维码信息的,它与QRScannerController类中的messageLabel相关联。

正如我前面提到的,我们依靠AVFoundation框架实现二维码扫描功能。首先打开QRScannerController.swift文件并导入框架:


import AVFoundation


然后,我们需要导入AVCaptureMetadataOutputObjectsDelegate协议,稍后会这个,然后更新下面这行代码:


class ViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate


接着需要在QRScannerController类中声明变量。我们会一个一个的讨论。



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


实现视频捕捉

我们需要实例化一个AVCaptureSession对象来进行视频捕捉。把下面的代码插入到QRScannerController类的viewDidLoad方法中:


// Get an instance of the AVCaptureDevice class to initialize a device object and provide the video as the media type parameter.        
         let captureDevice = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeVideo)        
                  
         do          {        
                  // Get an instance of the AVCaptureDeviceInput class using the previous device object.        
                  let input =          try          AVCaptureDeviceInput(device: captureDevice)        
                  
                  // Initialize the captureSession object.        
                  captureSession = AVCaptureSession()        
                  
                  // Set the input device on the capture session.        
                  captureSession?.addInput(input)        
                  
         }          catch          {        
                  // If any error occurs, simply print it out and don't continue any more.        
                  print(error)        
                  return        
         }


一个AVCaptureDevice代表一个物理设备。您使用捕捉设备来配置底层硬件的属性。我们通过调用defaultDevice(withMediaType:)的方法来获取要捕捉的视频数据,通过AVMediaTypeVideo来获得视频捕捉设备。

为了实现实时捕捉,我们实例化一个AVCaptureSession对象并添加视频捕捉设备的输入。AVCaptureSession对象是用来协调从视频输入设备到输出数据流。

这种情况下,这段会话的输出设置为一个AVCaptureMetaDataOutpu对象。AVCaptureMetaDataOutput类是二维码识别的核心部分。AVCaptureMetaDataOutput类与AVCaptureMetadataOutputObjectsDelegate协议相结合,用于截获输入设备中被发现的任何元数据(由设备的摄像头所捕获的二维码)并将其翻译成人类可读的形式。

不要担心一些东西听起来怪异或者你现在不能完全理解,接下来所有的事情都会变得清晰起来。现在,继续添加下面的代码到viewDidLoad方法中的do block中。


let captureMetadataOutput = AVCaptureMetadataOutput()        
         captureSession?.addOutput(captureMetadataOutput)


接下来,继续添加如下所示的代码。我们把self作为captureMetadataOutput对象的代理。这就是为什么QRReaderViewController类要遵循AVCaptureMetadataOutputObjectsDelegate协议。


captureMetadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)        
         captureMetadataOutput.metadataObjectTypes = [AVMetadataObjectTypeQRCode]


当捕获新的元数据对象时,他们被发送到代理对象中做进一步处理。在上述代码中我们指定执行委托的方法的调度队列。调度队列可以是串行或者并行的。根据苹果的文档,队列必须是串行的。所以我们用DispatchQueue.main来获取默认串行队列。Metadataobjecttypes类型也非常重要,它通知的程序那种是我们感兴趣的元数据。AVMetadataObjectTypeQRCode明确的表明的我们的目的,我们要做二维码扫描。

现在我们已经设置和配置了一个AVCaptureMetadataOutput对象,我们需要在屏幕上显示通过设备摄像头捕捉的视频。这可以通过AVCaptureVideoPreviewLayer实现,他的本质是一个CALayer。你使用与预览层结合的一个视频捕获会话显示视频。预览层作为当前视图的底层。在do-catch block中插入代码:


videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)        
         videoPreviewLayer?.videoGravity = AVLayerVideoGravityResizeAspectFill        
         videoPreviewLayer?.frame = view.layer.bounds        
         view.layer.addSublayer(videoPreviewLayer!)


最后,我们通过调用startrunning方法启动视频捕获:


captureSession?.startRunning()


如果你在真正iOS设备上编译并运行这个app,它会崩溃并提示以下错误:

This app has crashed because it attempted to access privacy-sensitive data without a usage description.  The app's Info.plist must contain an NSCameraUsageDescription key with a string value explaining to the user how the app uses this data.

类似于音频录制章节中所说,iOS要求开发者需要获取用户允许访问摄像头的权限。这样的话,你必须在Info.plist文件中添加一个NSCameraUsageDescription字段。打开文件,右键单击任何空白位置,添加新行。设置Privacy – Camera Usage Description的键和We need to access your camera for scanning QR code的值。


完成编辑设置app,然后在真正的设备上运行它。点击扫描按钮应该开启内置的摄像头并开始捕捉视频。然而在这一时刻,message label和状态栏是隐藏的。你可以通过添加下面的代码来修改它。这将是massage label 和状态栏出现在视频层的最上面。


view.bringSubview(toFront: messageLabel)        
         view.bringSubview(toFront: topbar)


在修改之后重新运行app。Message label中会出现“没有检测到二维码”并显示在屏幕上。

实现二维码识别

截至目前,这个app看起来相当像一个视频捕捉app。它是如何扫描二维码并翻译成有用的信息的呢?app本身就可以识别二维码,只是我们不知道而已。下面就是如何来调整app:

  • 当二维码呗检测是,app使用绿色的边框来图示显示编码
  • 二维码将被解码并且解码后的信息将显示在屏幕的底部

初始化绿色边框

为了突出二维码,我们先创建一个UIview对象并使它的边界为绿色。添加下面的代码到viewDidLoad方法中的do block里面:


qrCodeFrameView = UIView()        
                  
         if          let qrCodeFrameView = qrCodeFrameView {        
                  qrCodeFrameView.layer.borderColor = UIColor.green.cgColor        
                  qrCodeFrameView.layer.borderWidth = 2        
                  view.addSubview(qrCodeFrameView)        
                  view.bringSubview(toFront: qrCodeFrameView)        
         }


qrCodeFrameView的变化在屏幕上是看不见的因为UIview对的的默认大小设置为0。然后,当检测二维码的时候,我们会改变它的大小并将它变成一个绿色的盒子。

二维码解码

如前所述,当AVCaptureMetadataOutput对象识别二维码时,AVCaptureMetadataOutputObjectsDelegate代理方法会被调用。


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


到目前为止,我们还没有实现解码的方法,这就是为什么app不能翻译二维码。为了实现二维码的解码信息,我们需要实现方法对元数据对象进行额外的处理。这里是代码:func captureOutput(_ 


fun captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [Any]!, from connection: AVCaptureConnection!) {        
                  
                  if          metadataObjects == nil || metadataObjects.count == 0 {        
                  qrCodeFrameView?.frame = CGRect.zero        
                  messageLabel.text =          "No QR code is detected"        
                  return        
                  }        
                  
                  // Get the metadata object.        
                  let metadataObj = metadataObjects[0] as! AVMetadataMachineReadableCodeObject        
                  
                  if          metadataObj.type == AVMetadataObjectTypeQRCode {        
                  // If the found metadata is equal to the QR code metadata then update the status label's text and set the bounds        
                  let barCodeObject = videoPreviewLayer?.transformedMetadataObject(         for         : metadataObj)        
                  qrCodeFrameView?.frame = barCodeObject!.bounds        
                  
                  if          metadataObj.stringValue != nil {        
                  messageLabel.text = metadataObj.stringValue        
                  }        
                  }        
         }


方法里的第二个参数(即metadataObjects)是一个对象数组,它包含所有已读取的元数据对象。我们要做的第一件事就是确保数组不是空的,并且它包含至少一个对象。否则,我们会将qrCodeFrameView的大小复位为0并把message label写入默认信息。

如果发现了元数据,我们要检查它是否是一个二维码。如果是二维码的话,我们会继续寻找二维码的边界。这几行代码是用来设置突出二维码的绿色盒子的。通过调用viewpreviewlayer的transformedmetadataobject(:)方法,将元数据对象的视觉特性转换为层坐标。所以,我们可以从构建的绿色盒子里找到二维码的边界。

最后,我们把二维码解码成人类可读的信息。这一步应该非常简单。被解码的信息可以通过使用AVMetadataMachineReadableCode对象的stringValue属性来访问。

现在你已经准备好了!点击运行按钮在真实设备上编译和运行app吧。


app开启后,点击扫描按钮,将设备对准图中的二维码。app理解检测编码并解码该信息。


练习

演示app目前只可以扫描识别二维码。如果你可以把它变成一个普通条形码的识别器是不是也非常伟大。除了二维码,AVFoundation框架支持以下类型的条形码:

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


你的任务是调整现有的Xcode项目,使演示应用可以扫描其他类型的条形码。你需要使capturemetadataoutput识别出条码类型的数组而不仅仅是二维码数组。


captureMetadataOutput.metadataObjectTypes = [AVMetadataObjectTypeQRCode]


问题留给你自己去解决。即使我在下面的Xcode项目中提供了解决方案,我也鼓励你自己去寻找解决方法。这将非常有趣,并且理解代码的最好的方法就是了解代码是如何操作的。