用Swift开发二维码扫描器教程
(原文:Building a QR Code Reader in Swift 作者:Simon Ng 译者:xiaoying )
我相信大多数人都知道二维码(QR code)是什么,如果你对这个概念还不甚了解,那么看看下边那张图就知道了。
二维码是在二维平面里展示的一种条形码,开发者是Denso。最初它只是在制造业用来进行零部件跟踪,但是随着时间的发展,今天二维码已经在消费领域变得非常流行,在消费领域二维码通常会被用来编码一个登录页面或者推广页面的URL。与传统的条形码不同的是,二维码在水平和垂直方向上都可以存储信息,这样做的直接好处就是在二维码里可以同时以数字和字符的格式存储大量的信息。但是在这里我不会去探讨太多二维码的技术细节。如果感兴趣,可以去二维码的官方网站了解更多信息。
最近几年,二维码的应用不断的在增多。它可能出现在杂志、报纸、广告、广告板甚至出现在名片上。作为一个iOS开发者,你可能在想如何才能让你的应用具备识别二维码的功能呢。不久之前,Gabriel写了一篇很好的二维码入门指南。在本篇文章里,我们将使用Swift构建一个相似功能的二维码扫描器应用。在阅读完这篇文章之后,你就会了解怎么使用AVFoundation框架实时地去检测和识别二维码。
那么我们这就开工了。
Demo应用
我们要构建的这个demo应用相当的简单且直观,但是在开始讨论这个demo应用之前,我们要知道,在iOS里任何条码的扫描都是完全基于视频捕捉的,这很重要,这也是为什么我们要在含有二维码扫描的应用里加入AVFoundation框架。记住这一点,会对理解整个章节很有帮助。
那么,这个demo应用是如果工作的呢?
看一下下面这张截图,这就是这个应用的UI的样子。这个应用其实就像一个普通的视频捕捉应用,只是没有录像功能。当应用启动之后,它利用iPhone的摄像头来对准二维码,然后二维码会自动被识别,解码后的信息(例如,一个URL)就会显示在屏幕的下方。
我已经预先创建好了一个模板工程,并将storyboard和显示信息的label都帮你连接好了,你可以先从这里下载这个工程。
使用AVFoundation框架
在上面下载的模板工程里,我已经创建了这个应用的用户接口。在UI界面的那个label会被用来显示二维码解码后的信息,这个label已经和ViewController里的messageLabel这个属性进行绑定。
就像之前说过的,我们要依靠AVFoundation框架来实现二维码扫描的功能,所以首先,打开ViewController.swift文件,导入框架:
import AVFoundation
然后,我们需要实现AVCaptureMetadataOutputObjectsDelegate协议,我们一会儿再说这个协议,现在,只要在代码里加上这一行:
class ViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate
在继续之前,先在ViewController类里定义一下变量,我们之后将会挨个讲解它们:
var captureSession:AVCaptureSession? var videoPreviewLayer:AVCaptureVideoPreviewLayer? var qrCodeFrameView:UIView?
实现视频捕获
就像在前面一段提到的,二维码的读取完全是基于视频捕获的,那么为了实时捕获视频,我们只需要以合适的AVCaptureDevice对象作为输入参数去实例化一个AVCaptureSession对象。在ViewController类的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.defaultDeviceWithMediaType(AVMediaTypeVideo) // Get an instance of the AVCaptureDeviceInput class using the previous device object. var error:NSError? let input: AnyObject! = AVCaptureDeviceInput.deviceInputWithDevice(captureDevice, error: &error) if (error != nil) { // If any error occurs, simply log the description of it and don't continue any more. println("\(error?.localizedDescription)") return } // Initialize the captureSession object. captureSession = AVCaptureSession() // Set the input device on the capture session. captureSession?.addInput(input as AVCaptureInput)
一个AVCaptureDevice对象代表了一个物理上的视频设备,在这里我们配置了一个默认的视频设备。由于我们将要捕获视频数据,所以我们调用defaultDeviceWithMediaType方法和AVMediaTypeVideo来得到视频设备。我们以视频设备为输入参数去实例化了一个AVCaptureSession会话,用它来实现实时视频捕获。AVCaptureSession会话是用来管理视频数据流从输入设备传送到输出端的会话过程的。
在这里,这个会话的输出端被设定为一个AVCaptureMetaDataOutput对象,而这个AVCaptureMetaDataOutput类是二维码读取的核心组成部分,它和AVCaptureMetadataOutputObjectsDelegate协议一起,将被用来获取从输入设备传过来的元数据(就是摄像头捕获的二维码)然后将它们翻译为人类可读的格式。如果你觉得这些听起来很奇怪或者你现在根本听不懂,不要担心,一会儿这些都会变得很清晰。现在要做的就是继续将下面的代码加入viewDidLoad方法中去:
// Initialize a AVCaptureMetadataOutput object and set it as the output device to the capture session. let captureMetadataOutput = AVCaptureMetadataOutput() captureSession?.addOutput(captureMetadataOutput)
然后,接着把下面的代码也加进去。在这里我们把self设置为captureMetadataOutput对象的代理。这就是为什么ORReaderViewController类要实现AVCaptureMetadataOutputObjectsDelegate协议,当新的元数据对象被捕获到时,它们就被转发到这个代理的方法中去。根据苹果的文档,这个队列必须是串行的,所以我们直接使用dispatch_get_main_queue()获取默认的GCD的串行执行队列。
// Set delegate and use the default dispatch queue to execute the call back captureMetadataOutput.setMetadataObjectsDelegate(self, queue: dispatch_get_main_queue()) captureMetadataOutput.metadataObjectTypes = [AVMetadataObjectTypeQRCode]
metadataObjectTypes属性也非常重要,因为它的值会被用来判定整个应用程序对哪类元数据感兴趣。在这里我们将它指定为AVMetadataObjectTypeQRCode。
现在我们完成了对AVCaptureMetadataOutput对象的设置,我们还需要在屏幕上显示摄像头捕获到的图像,这可以通过AVCaptureVideoPreviewLayer(其实就是一个CALayer)来完成。然后使用这个预览图层和图像信息捕获会话来显示视频,这个预览图层要作为当前视图的子图层添加进去,下面是相关代码:
// Initialize the video preview layer and add it as a sublayer to the viewPreview view's layer. videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession) videoPreviewLayer?.videoGravity = AVLayerVideoGravityResizeAspectFill videoPreviewLayer?.frame = view.layer.bounds view.layer.addSublayer(videoPreviewLayer)
我们终于能捕获视频了,这里要调用视频捕获回话的startRunning方法来启动它:
// Start video capture. captureSession?.startRunning()
如果你编译运行这个应用,它应该在启动之后就开始捕获视频了。但是,等等,好像下面显示消息的label不见了。
可以添加如下代码来让它显示:
// Move the message label to the top view view.bringSubviewToFront(messageLabel)
修改后重新运行程序,label上这是应该会显示“No QR code is detected”(没有检测到二维码)。
实现二维码读取
好了,现在这个应用看起来已经像一个视频捕获应用了,那么怎么样它才能扫描二维码并且翻译成有意义的明文呢?这个应用本身已经具备了检测二维码的能力,只是我们还不知道,这里是我们将要对应用做的改变:
1. 当检测到二维码时,应用会用一个绿色方框圈住二维码。
2. 这个二维码将被解码,然后将解码的信息显示在屏幕的下方。
初始化绿色方框
为了圈住二维码,我们首先创建一个UIView对象,并将它的边框设为绿色。在viewDidLoad方法中加入如下代码:
// Initialize QR Code Frame to highlight the QR code qrCodeFrameView = UIView() qrCodeFrameView?.layer.borderColor = UIColor.greenColor().CGColor qrCodeFrameView?.layer.borderWidth = 2 view.addSubview(qrCodeFrameView!) view.bringSubviewToFront(qrCodeFrameView!)
现在这个UIView是隐形的,因为它的尺寸默认会被设成零。之后,当检测到二维码时,我们再改变它的尺寸,那么它就会变成一个绿色的方框了。
解码二维码
像之前提到的,当AVCaptureMetadataOutput对象识别出来一个二维码,下边的方法(AVCaptureMetadataOutputObjectsDelegate的代理方法)就会被调用:
optional func captureOutput(_ captureOutput: AVCaptureOutput!,? didOutputMetadataObjects metadataObjects: [AnyObject]!,? fromConnection connection: AVCaptureConnection!)
到现在为止我们还没有实现这个方法,这就是为什么我们不能翻译这个二维码。为了进一步捕获并且解码二维码,我们需要实现这个方法,对元数据对象进行进一步操作。下边是相关代码:
func captureOutput(captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [AnyObject]!, fromConnection connection: AVCaptureConnection!) { // Check if the metadataObjects array is not nil and it contains at least one object. if metadataObjects == nil || metadataObjects.count == 0 { qrCodeFrameView?.frame = CGRectZero 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?.transformedMetadataObjectForMetadataObject(metadataObj as AVMetadataMachineReadableCodeObject) as AVMetadataMachineReadableCodeObject qrCodeFrameView?.frame = barCodeObject.bounds; if metadataObj.stringValue != nil { messageLabel.text = metadataObj.stringValue } } }
这个方法的第二个参数(就是metadataObjects)是一个Array数组,它包含了所有已被读取的元数据对象。当然,首先要做的就是要判断这个数组是否为空。如果为空,我们就要重置qrCodeFrameView的尺寸为零,并且把messageLabel的内容设为默认内容。
如果数组里有元数据,我们就去判断它是否是二维码。如果是,我们接着就去找到二维码的边界。
这几行代码用来设置圈住二维码的绿色方框。通过调用viewPreviewLayer的transformedMetadataObjectForMetadataObject方法,元数据对象就会被转化成图层的坐标。通过这个坐标,我们可以获取二维码的边界并构建绿色方框。
let barCodeObject = videoPreviewLayer?.transformedMetadataObjectForMetadataObject(metadataObj as AVMetadataMachineReadableCodeObject) as AVMetadataMachineReadableCodeObject qrCodeFrameView?.frame = barCodeObject.bounds
最后,我们对二维码进行解码,得到人类可读信息。解码信息可以用过访问AVMetadataMachineReadableCodeObject 对象的stringValue属性得到,非常简单。
现在,一切准备就绪,点击Run按钮来编译并在真实设备上运行这个应用。软件打开后,对着一个二维码,这个应用就会马上检测到并且完成解码。
提示:你也可以通过http://www.qrcode-monkey.com来生成你自己的二维码,非常简单。
总结
现在,通过使用AVFoundation框架去创建了一个二维码扫描应用变得前所未有的简单。另外除了二维码,这个框架还支持很多别的条码类别,例如Code39,Code128,Aztec,和PDF417。大家可以尝试修改这个Xcode工程来实现这些类型的条码扫描。
在这里你可以下载本文所述的完整的工程代码,仅供参考。