用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工程來實現這些類型的條碼掃描。
在這里你可以下載本文所述的完整的工程代碼,僅供參考。