原文: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上下載解決方案。