首先,什麼是QR Code?我相信你們大多數都已經知道什麼是QR Code了。倘若你還沒有聽過,看一下以下的這張圖,這就是QR Code。
QR(Quick Response 的縮寫)Code 是由 Denso 所開發的一個二維條碼(2-dimensional bar code)。原來的用途是設計作為製造業做零件追蹤用,QR Code 近幾年已經逐漸在消費市場流行,主要是將 URL 編碼,作為網頁導引或提供市場資訊用。不像傳統所熟悉的條碼,QR Code 包含了水平與垂直方向的資訊。也因此造就它可以擁有較多數字與文字資料量的儲存能力。我不想要在這邊討論QR Code 的技術細項。倘若你有興趣的話,你可以至 QR Code 的官方網站學習更多相關的知識。
因為 iPhone 與 Android 手機的普及,QR Code 的使用顯著增加。在某些國家,QR Code 幾乎隨處可見。不管是雜誌、報紙、廣告、看板,甚至名片都有。作為一個 iOS 開發者,你可能想知道要如何強化你的 App ,讓它能夠讀取 QR Code。在 iOS 7 以前,你需要依賴第三方函式庫才能實作掃瞄的功能。現在,你可以使用 AVFoundation 框架來即時掃瞄與讀取條碼。
建立一個可以掃瞄 App 與轉譯 QR Code 已經再容易不過了。
建立一個QR Code讀取器App
我們準備要建立的App十分簡單且容易。在我們繼續討論這個範例App之前,要知道任何在iOS上的條碼掃瞄,包括QR Code 在內,全部都是以視訊擷取為基礎。這也是為何條碼掃瞄功能,是含括在AVFoundation 框架中。有了這個概念之後,有助於您了解整章的內容。
那麼,App範例要如何運作呢?
以下的螢幕截圖,是這個App UI的樣貌。這個App運作起來與影片擷取App非常相像,只是沒有錄製的功能,當App打開後,它使用了iPhone背後鏡頭來聚焦與自動辨識QR Code,解碼(decode)資訊(也就是一個URL),顯示在畫面底部的地方。
就是這麼簡單。
請你先下載我所準備的專案模板來開始這個專案,我已經預建了Storyboard 並幫你加上一個訊息標籤。
好的,我們開始在 App端中開發QR Code掃描功能吧。
導入AVFoundation 框架
我已經在這個App專案模板中建立了這個App的使用者介面。在這個UI中的標籤,是用來顯示QR Code解碼後的資訊,而它與ViewController
類別的messageLabel
屬性相關聯。
如同之前所提,我們依靠AVFoundation 框架來實作 QR Code 掃描功能。首先,打開ViewController.swift 檔案來導入此框架。
import AVFoundation
接著,我們需要實作AVCaptureMetadataOutputObjectsDelegate
協定。我們待會會討論到。現在更新以下這行程式:
class ViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate
繼續往下之前,在ViewController
類別宣告下面的變數。我們稍後會一個接一個說明。
var captureSession:AVCaptureSession?
var videoPreviewLayer:AVCaptureVideoPreviewLayer?
var qrCodeFrameView:UIView?
實作影像擷取
如同前面章節所談到,QR Code的讀取是由影像來擷取。要執行即時的擷取,我們只需要實體化一個AVCaptureSession
物件加上輸入設定來讓AVCaptureDevice
順利進行影像擷取。插入以下的程式在ViewController
類別的viewDidLoad 方法:
// 取得 AVCaptureDevice 類別的實體來初始化一個device物件,並提供video
// 作為媒體型態參數
let captureDevice = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo)
// 使用前面的 device 物件取得 AVCaptureDeviceInput 類別的實體
var error:NSError?
let input: AnyObject! = AVCaptureDeviceInput.deviceInputWithDevice(captureDevice, error: &error)
if (error != nil) {
// 假如有錯誤產生、 單純記錄其狀況,不再繼續。
println("\(error?.localizedDescription)")
return
}
// 初始化 captureSession 物件
captureSession = AVCaptureSession()
// 在capture session 設定輸入裝置
captureSession?.addInput(input as AVCaptureInput)
AVCaptureDevice
物件代表一個實體擷取裝置。你使用一個擷取裝置來設置底層硬體的屬性。因為我們準備要擷取影像資料,我們呼叫defaultDeviceWithMediaType
方法,傳遞AVMediaTypeVideo
以取得影像擷取裝置。要執行即時擷取,我們實體化一個AVCaptureSession
物件,並加上要輸入的影像擷取裝置。AVCaptureSession
物件是用來協調來自影像輸入裝置至輸出的資料流程。
在這裡,session
的輸出裝置是設定為AVCaptureMetaDataOutput
物件。AVCaptureMetaDataOutput
類別是QR Code讀取的核心部分。這個類別,結合了AVCaptureMetadataOutputObjectsDelegate
協定,用來攔截(intercept)任何來自輸入裝置所發現的元資料(metadata),也就是裝置的相機所擷取的QR Code,並將其轉譯為人們可以閱讀的格式。倘若你聽起來很奇怪或無法馬上完全理解的話,請別擔心,不久一切會逐漸明朗。現在繼續加入以下的程式至viewDidLoad
方法中:
// 初始化 AVCaptureMetadataOutput 物件並將其設定作為擷取session的輸出裝置
let captureMetadataOutput = AVCaptureMetadataOutput()
captureSession?.addOutput(captureMetadataOutput)
接著,繼續加入如下的程式。我們設定 self
作為captureMetadataOutput
物件的代理。這是QRReaderViewController
類別採用AVCaptureMetadataOutputObjectsDelegate
協定的原因。當一個新的元資料被擷取時,便會將其轉交給代理物件做進一步處理。這裏我們也需要執行代理的方法來指定調度佇列(dispatch queue),依照Apple的文件,此佇列必須是串列佇列(serial queue)。因此我們只是使用dispatch_get_main_queue()
函數來取得預設的串列佇列。
// 設定代理並使用預設的調度佇列來執行回呼(call back)
captureMetadataOutput.setMetadataObjectsDelegate(self, queue: dispatch_get_main_queue())
captureMetadataOutput.metadataObjectTypes = [AVMetadataObjectTypeQRCode]
metadataObjectTypes
屬性也是非常重要:因為這是我們告訴App哪一種元資料是我們感興趣的地方。AVMetadataObjectTypeQRCode
清楚的指明我們的需求。
現在我們設定與設置一個AVCaptureMetadataOutput
物件。我們需要透過裝置的相機將擷取的影像顯示在畫面上。這可以使用AVCaptureVideoPreviewLayer
來完成,實際上它是 CALayer
。你使用預覽層(preview layer)結合AV capture session 來顯示影像。這個預覽層被加入作為目前視圖的子層(sublayer),以下為其程式:
// 初始化影像預覽層,並將其加為 viewPreview 視圖層的子層
videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
videoPreviewLayer?.videoGravity = AVLayerVideoGravityResizeAspectFill
videoPreviewLayer?.frame = view.layer.bounds
view.layer.addSublayer(videoPreviewLayer)
最後,我們透過呼叫capture session的startRunning
方法來開始影像擷取:
// 開始影像擷取
captureSession?.startRunning()
倘若你編譯與執行App,打開後便會開始捕捉影像。不過,等一下,似乎訊息標籤被隱藏起來。
你可以加入以下的程式來修復它:
// 將訊息標籤移到最上層視圖
view.bringSubviewToFront(messageLabel)
改變完成後,重新執行App。現在訊息標籤「No QR code is detected」,便會出現在畫面上了。
實作 QR Code 讀取功能
現在,看起來很像影片擷取App。那麼該如何掃描QR Code 並將QR編碼轉譯成有意義的內容?這個App 自己已經能夠偵測QR Code,我們只是還沒察覺到,不過,以下是我們準備要去調整的部分:
- 當QR Code被偵測後,App會將QR Code以綠色方框突顯出來。
- QR Code會被解碼,而解碼資訊會顯示在畫面底部。
綠色方框的初始化
為了突顯QR Code,我們先建立一個UIView
物件,並將其邊框設為綠色。將以下的程式加進viewDidLoad
方法中:
// 初始化 QR Code Frame 來突顯 QR code
qrCodeFrameView = UIView()
qrCodeFrameView?.layer.borderColor = UIColor.greenColor().CGColor
qrCodeFrameView?.layer.borderWidth = 2
view.addSubview(qrCodeFrameView!)
view.bringSubviewToFront(qrCodeFrameView!)
這個 UIView
在畫面上是看不見的,因為qrCodeFrameView
物件的大小預設是零。接著,倘若QR Code被偵測到,我們將改變他的大小並將其轉成綠色方框。
QR Code 解碼
如同之前所提,當AVCaptureMetadataOutput
物件辨識一個QR Code時,以下AVCaptureMetadataOutputObjectsDelegate
的代理方法會被呼叫:
optional func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [AnyObject]!, fromConnection connection: AVCaptureConnection!)
到目前為止,我們還沒有執行這個方法,這也是為何這個App無法轉譯QR Code的原因,為了擷取QR Code並進行資訊解碼,我們需要另外實作此方法,來執行元資料物件的處理程序。以下是其程式碼:
func captureOutput(captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [AnyObject]!, fromConnection connection: AVCaptureConnection!) {
// 檢查 metadataObjects 陣列是否為非空值,它至少需包含一個物件
if metadataObjects == nil || metadataObjects.count == 0 {
qrCodeFrameView?.frame = CGRectZero
messageLabel.text = "No QR code is detected"
return
}
// 取得元資料(metadata)物件
let metadataObj = metadataObjects[0] as AVMetadataMachineReadableCodeObject
if metadataObj.type == AVMetadataObjectTypeQRCode {
//倘若發現的原資料與 QR code 原資料相同,便更新狀態標籤的文字並設定邊界
let barCodeObject = videoPreviewLayer?.transformedMetadataObjectForMetadataObject(metadataObj as
AVMetadataMachineReadableCodeObject) as AVMetadataMachineReadableCodeObject
qrCodeFrameView?.frame = barCodeObject.bounds;
if metadataObj.stringValue != nil {
messageLabel.text = metadataObj.stringValue
}
}
}
方法的第二個參數(也就是metadataObjects
)是一個陣列物件,包含了所有已經讀取的元資料物件。最先要做的事,就是確認這個陣列不是空值(nil),至少需包含一個物件。否則的話我們將重置qrCodeFrameView
的大小設為零,並設定messageLabel
為其預設訊息。
倘若有發現到元資料,我們檢查是否為QR Code,倘若是的話,我們將繼續找到QR Code的邊界。
這幾行程式是被使用作為突顯QR Code 綠色方框的設定。透過呼叫viewPreviewLayer
的transformedMetadataObjectForMetadataObject
方法,元資料物件的視覺屬性被轉換為層座標(layer coordinates),也因此我們可以找到QR Code的邊界以建構綠色方框。
let barCodeObject = videoPreviewLayer?.transformedMetadataObjectForMetadataObject(
metadataObj as AVMetadataMachineReadableCodeObject) as AVMetadataMachineReadableCodeObject
qrCodeFrameView?.frame = barCodeObject.bounds
最後,我們將QR Code解碼成為人們可以閱讀的資訊。這非常簡單,解碼的資訊可以由AVMetadataMachineReadableCodeObject
的stringValue
屬性來存取。
現在可以準備開始測試了!按下Run按鈕,並在實體裝置中運行。打開後,將它面對QR Code,如下圖所示。此App 會立刻偵測到 QR Code,並進行資訊解碼。
你的作業 – 條碼閱讀器
本章範例的App主要是掃描QR Code。倘若你能把它變成一般的條碼閱讀器是不是也不錯?除了QR Code 之外,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
來識別條碼型態的陣列,而不是QR Code。
captureMetadataOutput.metadataObjectTypes = barcodeTypes
我將留給你自己找出解決方案,我鼓勵你能夠自己找出問題,這會很有趣,你將會從中學習到很多經驗。無論如何,為了進一步讓您參考,你可以從這裡下載完整的 Xcode 專案。
原文: Building a QR Code Reader in Swift