原文: Building a Barcode and QR Code Reader in Swift 3 and Xcode 8
译者: Fairy-happy
什么是二维码?我相信大多数人都知道二维码是什么。即使你没有听说过二维码,但是看看上面的图片,你会恍然大悟,这就是二维码!
QR(Quick Response 的缩写)码是由Denso开发的一种二维条形码。二维码最初是为了跟踪零部件制造,近几年来,二维码作为一种编码着登录信息或者营销信息链接的识别码普及到了消费领域。与大众熟悉的条形码不同,二维码在水平和垂直两个方向上都包含了信息。这也有助于二维码以数字和字母的形式存储大量的数据。我并不想在这里谈论二维码的技术细节,如果你感兴趣可以去查阅 二维码的官方网站 。
编者按:这是 Intermediate iOS Programming with Swift book 这本书的样章。
随着iPhone和安卓手机的盛行,二维码的使用大幅增加。在一些国家,二维码的踪迹随处可见。它们出现在杂志、报纸、广告、广告牌、名片,甚至食物菜单上。作为一个iOS开发者,你可能会想知道怎样让你的app读取二维码。在iOS7之前,你不得不依赖第三方库来实现扫描功能。现在,你可以使用内置的AVFoundation框架来发现和实时读取条形码。
创建一个扫描和翻译的二维码的app从未如此加简单。
小提示:你可以在 这个网站 生成自己二维码。
创建一个二维码识别App
我们要建立的演示app非常简单明了。在开始创建演示app之前,我们要非常清楚的理解,在iOS中任何的条形码扫描,包括二维码扫描,都是基于视频捕捉的。这也是为什么条形码扫描功能添加在AVFoundation框架之中。将这条铭记于心,它会帮助你理解整篇文章。
那么,demo app是怎样工作的呢?
下边的截图展示了APP的UI。这个应用相当于一个没有记录功能的视频捕捉应用。当应用程序启动时,它利用iPhone的后置摄像头自动识别二维码。被解码的信息(例如一个网址)显示在屏幕的右下方。
就是这么简单。
要建立应用程序,你可以从 这里 下载项目模板。我已经预先创建了storyboard并且连接了一个信息 label。(我已经预先创建好了 storyboard 以及一个信息 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?
实现视频捕捉
在前一节提到过,二维码识别完全是依赖视频捕捉的。为了进行实时捕捉, 我们需要实例化一个有适当的输入设置的AVCaptureDevice的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中。
// Initialize a AVCaptureMetadataOutput object and set it as the output device to the capture session. let captureMetadataOutput = AVCaptureMetadataOutput() captureSession?.addOutput(captureMetadataOutput)
接下来,继续添加如下所示的代码。我们把self作为captureMetadataOutput对象的代理。这就是为什么QRReaderViewController类要遵循AVCaptureMetadataOutputObjectsDelegate协议的原因。
// Set delegate and use the default dispatch queue to execute the call back captureMetadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main) captureMetadataOutput.metadataObjectTypes = [AVMetadataObjectTypeQRCode]
当捕获新的元数据对象时,它们被发送到代理对象中做进一步处理。在上述代码中我们指定执行委托的方法的调度队列。调度队列可以是串行或者并行的。根据苹果的文档,队列必须是串行的。所以我们用DispatchQueue.main来获取默认串行队列。Metadataobjecttypes类型也非常重要,它告诉程序我们对哪种元数据感兴趣。AVMetadataObjectTypeQRCode明确的表明了我们的目的,我们需要二维码扫描。
现在我们已经设置和配置了一个AVCaptureMetadataOutput对象,接下来我们需要在屏幕上显示通过设备摄像头捕捉的视频。这可以通过AVCaptureVideoPreviewLayer来实现,它的本质是一个CALayer。你使用预览层与一个视频捕获session来显示视频。预览层作为当前视图的sublayer。在do-catch block中插入代码:
// 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()
如果你在真正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 和状态栏出现在视频层的最上面。
// Move the message label and top bar to the front view.bringSubview(toFront: messageLabel) view.bringSubview(toFront: topbar)
在修改之后重新运行app。Message label中会出现"没有检测到二维码"并显示在屏幕上。
实现二维码识别
截至目前,这个app看起来相当像一个视频捕捉app。它是如何扫描二维码并翻译成有用的信息的呢?App本身就可以识别二维码,只是我们不知道而已。下面我们要对app进行调整:
当二维码被检测到时,app使用绿色边框来高亮显示。
对二维码进行解码,并且解码后的信息将显示在屏幕的底部。
为了突出二维码,我们先创建一个UIview对象,并将其边界设为绿色。添加下面的代码到viewDidLoad方法中的do block里面:
// Initialize QR Code Frame to highlight the QR code 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(_ captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [Any]!, from 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 = 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(:)方法,将元数据对象的可视属性 (visual properties) 转换为层坐标。所以,我们可以从构建的绿色边框里找到二维码的边界。
最后,我们把二维码解码成人类可读的信息。这一步应该非常简单。被解码的信息可以通过使用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识别出条码类型的数组而不仅仅是二维码数组。
我会把问题留给你自己去解决。即使我在下面的Xcode项目中提供了解决方案,我也鼓励你自己去寻找解决方法。这将非常有趣,并且理解代码的最好的方法就是了解代码是如何操作的。
如果你还是难住了,你可以在 Github 上下载解决方案。