第 11 章
使用 AVFoundation 框架進行 QR Code 掃描
那麼,什麼是 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 已經再容易不過了。
Quick tip: 你可以自己建立 QR Code,只要至 http://www.qrcode-monkey.com 便可以自己做一個 。
建立一個 QR Code 讀取器 App
我們準備要建立的 App 十分簡單且容易。在我們繼續建立這個範例 App 之前,你必須要知道任何在 iOS 上的條碼掃瞄,包括 QR Code 在內,全部都是以視訊擷取為基礎。這也是為何條碼掃瞄功能,是含括在 AVFoundation 框架中。有了這個概念之後,會有助於了解整章的內容。
那麼,App 範例要如何運作呢?
下面的螢幕截圖,是這個 App UI 的樣貌。這個 App 運作起來與影片擷取 App 非常相像,只是沒有錄製的功能,當 App 開啟後,它利用了iPhone 後置鏡頭來聚焦與自動辨識 QR Code,解碼(decode)資訊(例如一個 URL),並顯示在畫面底部的地方。
就是這麼簡單。
要建立這個 App,你可以至 http://www.appcoda.com/resources/swift59/QRCodeReaderStarter.zip 下載專案模板來開始。模板中已經預建了 Storyboard 並幫你加上一個訊息標籤。主畫面是關聯 QRCodeViewController
類別,而掃瞄器畫面是關聯 QRScannerController
類別。
你可以執行起始專案來看一下,在啟動 App 後,你可以按下掃描按鈕來帶出掃描視窗。接下我們會實作這個視圖控制器來進行QR Code 掃描。
現在你已經瞭解起始專案的運作模式,我們來開始在 App 中開發QR 掃描功能。
匯入 AVFoundation 框架
我已經在這個 案模板中建立了 App 的使用者介面。在這個 UI 中的標籤,是用來顯示 QR Code 解碼後的資訊,而它與 QRScannerController
類別的 messageLabel
屬性相關聯。
如同之前所提,我們依靠 AVFoundation 框架來實作 QR Code 掃描功能。首先開啟 QRScannerController.swift
檔來匯入框架:
import AVFoundation
接著我們需要實作 AVCaptureMetadataOutputObjectsDelegate
協定。我們待會會討論到。現在以擴展來採用這個協定:
extension QRScannerController: AVCaptureMetadataOutputObjectsDelegate {
}
繼續往下之前,在 QRScannerController
類別宣告以下的變數。我們稍後會逐一做說明。
var captureSession = AVCaptureSession()
var videoPreviewLayer: AVCaptureVideoPreviewLayer?
var qrCodeFrameView: UIView?
實作影片擷取
如同前面章節所談到,QR Code 的讀取以影片擷取為基礎。要執行即時的擷取,我們只需要:
- 查詢後置鏡頭裝置
- 設定
AVCaptureSession
物件的輸入給對應的AVCaptureDevice
來擷取影片。
在 QRScannerController
類別的 viewDidLoad
方法插入以下的程式:
// 取得後置鏡頭來擷取影片
guard let captureDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else {
print("Failed to get the camera device")
return
}
do {
// 使用前一個裝置物件來取得 AVCaptureDeviceInput 類別的實例
let input = try AVCaptureDeviceInput(device: captureDevice)
// 在擷取 session 設定輸入裝置
captureSession.addInput(input)
} catch {
// 假如有錯誤產生、單純輸出其狀況不再繼續執行
print(error)
return
}
假設你有讀過前一章,你應該知道 AVCaptureDevice.default
類別是設計作為以特定裝置型態來找出所有可用的裝置。在上面的程式,我們指定取得支援媒體型態為 .video
的裝置。
要執行即時擷取時,我們使用 AVCaptureSession
物件,並加上影片擷取裝置的輸入。AVCaptureSession
物件是用來協調來自影片輸入裝置至輸出的資料流(flow of data)。
在這裡,session 的輸出是設定為一個 AVCaptureMetaDataOutput
物件。AVCaptureMetaDataOutput
類別是 QR Code 讀取的核心部分。 這個類別結合了 AVCaptureMetadataOutputObjectsDelegate
協定, 用來攔截(intercept) 任何來自輸入裝置所發現的元資料(metadata),也就是裝置的相機所擷取的 QR Code,並將其轉譯為人們可以閱讀的格式。
倘若你聽起來很奇怪或無法馬上完全理解的話,請別擔心,不久一切會逐漸明朗。現在繼續加入以下的程式至 viewDidLoad
方法的 do
區塊中:
// 初始化一個 AVCaptureMetadataOutput 物件並將其設定做為擷取 session 的輸出裝置
let captureMetadataOutput = AVCaptureMetadataOutput()
captureSession.addOutput(captureMetadataOutput)
接著繼續加入如下的程式。我們設定 self
做為 captureMetadataOutput
物件的委派。這也是為何 QRReaderViewController
類別採用 AVCaptureMetadataOutputObjectsDelegate
協定的原因。
// 設定委派並使用預設的調度佇列來執行回呼(call back)
captureMetadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
captureMetadataOutput.metadataObjectTypes = [AVMetadataObject.ObjectType.qr]
當一個新的元資料被擷取時,便會將其轉交給委派物件做進一步處理。在以上的程式中,我們指定調度佇列(dispatch queue)至執行委派的方法,佇列可以是串列佇列( serial dispatch)或者共時佇列(concurrent dispatch )是依照 Apple 的文件,這個佇列必須是串列佇列。所以我們使用 DispatchQueue.main
來取得預設的串列佇列。
metadataObjectTypes
屬性也是非常重要:因為這是我們告訴 App 哪一種元資料是我們感興趣的地方。AVMetadataObject.ObjectType.qr
清楚的指明我們的需求。我們想要進行QR code掃描。
現在我們設定與設置一個 AVCaptureMetadataOutput
物件。我們需要透過裝置的相機將擷取的影片顯示在畫面上。這可以使用 AVCaptureVideoPreviewLayer
來完成,實際上它是一個 CALayer
。你使用預覽層(preview layer)結合 AV 擷取 session 來顯示影片。這個預覽層被加入做為目前視圖的子層(sublayer),在 do-catch
區塊插入以下的程式碼:
// 初始化影片預覽層,並將其作為子層加入 viewPreview 視圖的圖層中
videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
videoPreviewLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill
videoPreviewLayer?.frame = view.layer.bounds
view.layer.addSublayer(videoPreviewLayer!)
最後,我們透過呼叫擷取 session 的 startRunning
方法來開始影片擷取:
// 開始影片的擷取
DispatchQueue.global(qos: .background).async {
self.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 需要 App 開發者在存取相機之前,先要取得使用者的允許。要這麼做的話,你必須要在 Info.plist
檔中加入一個名為 NSCameraUsageDescription
的 key。開啟這個檔案,並在任一個空白處加入一個新列。設定這個 key 為 Privacy - Camera Usage Description ,其值設為 We need to access your camera for scanning QR code 。
當你完成編輯之後,再次將 App 部署至實體裝置中,按下掃描按鈕,應該就會帶出內建的相機並開始擷取影片。不過,此時訊息標籤與頂部列被隱藏起來。你可以加入以下這行程式來修復它。這會將訊息標籤與頂部列移至影片層的上方。
//移動訊息標籤與頂部列至上層
view.bringSubviewToFront(messageLabel)
view.bringSubviewToFront(topbar)
改變完成後重新執行 App。現在畫面上的訊息標籤會顯示 No QR code is detected 了。
實作QR Code 讀取功能
這個 App 現在看起來很像影片擷取 App。那麼該如何掃描 QR Code 並將 QR 編碼轉譯成有意義的內容?這個 App 本身已經能夠偵測 QR Code,只是我們還沒察覺到。不過以下是我們準備要去調整的部分:
- 當 QR Code 被偵測後,App 會將 QR Code 以綠色方框突顯出來
- QR Code 會被解碼,而解碼資訊會顯示在畫面底部
綠色方框的初始化
為了突顯 QR Code,我們先建立一個 UIView
物件,並將其邊框設為綠色。將以下的程式加進 viewDidLoad
方法的 do
區塊中:
// 初始化 QR Code 框來突顯 QR code
qrCodeFrameView = UIView()
if let qrCodeFrameView = qrCodeFrameView {
qrCodeFrameView.layer.borderColor = UIColor.green.cgColor
qrCodeFrameView.layer.borderWidth = 2
view.addSubview(qrCodeFrameView)
view.bringSubviewToFront(qrCodeFrameView)
}
此 qrCodeFrameView
變數在畫面上是看不見的,因為 UIView
物件的大小預設是零。接著,倘若 QR Code 被偵測到,我們將改變它的大小並將其轉成綠色方框。
QR Code 解碼
如同之前所提,當 AVCaptureMetadataOutput
物件辨識一個 QR Code 時,以下 AVCaptureMetadataOutputObjectsDelegate
的委派方法會被呼叫:
optional func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection)
到目前為止,我們還沒有實作這個方法,這也是為何這個 App 無法轉譯 QR Code 的原因。為了擷取 QR Code 並進行資訊解碼,我們需要實作此方法,來執行元資料物件的處理程序。以下是其程式碼:
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
// 檢查 metadataObjects 陣列為非空值,它至少需包含一個物件
if metadataObjects.count == 0 {
qrCodeFrameView?.frame = CGRect.zero
messageLabel.text = "No QR code is detected"
return
}
// 取得元資料(metadata)物件
let metadataObj = metadataObjects[0] as! AVMetadataMachineReadableCodeObject
if metadataObj.type == AVMetadataObject.ObjectType.qr {
// 倘若發現的元資料與 QR code 元資料相同,便更新狀態標籤的文字並設定邊界
let barCodeObject = videoPreviewLayer?.transformedMetadataObject(for: metadataObj)
qrCodeFrameView?.frame = barCodeObject!.bounds
if metadataObj.stringValue != nil {
messageLabel.text = metadataObj.stringValue
}
}
}
方法的第二個參數(也就是 metadataObjects
)是一個陣列物件,包含了所有已經讀取的元資料物件。最先要做的事,就是確認這個陣列不是空值(nil)
,至少需包含一個物件。否則的話將重置 qrCodeFrameView
的大小設為零,並設定 messageLabel
為其預設訊息。
倘若有發現到元資料物件,我們檢查是否為 QR Code,倘若是的話,我們將繼續找到 QR Code 範圍的邊界。這幾行程式碼是被使用做為突顯 QR Code 綠色方框的設定。透過呼叫 viewPreviewLayer
的 transformedMetadataObject(for:)
方法,元資料物件的視覺屬性被轉換為層座標(layer coordinates),也因此我們可以找到 QR Code 的邊界以建構綠色方框。
最後,我們將 QR Code 解碼成為一般人可以閱讀的資訊。這個步驟非常簡單,解碼的資訊可以由 AVMetadataMachineReadableCode
物件的 stringValue 屬性來存取。
現在可以準備開始測試了!按下 Run 按鈕,並在實體裝置中運行這個 App。
開啟後,將它對準如圖11.4 的 QR Code 。此 App 會立刻偵測到 QR Code 並進行資訊解碼。
你的作業 – 條碼閱讀器
本章範例的 App 主要是掃描 QR Code。倘若你能把它變成一般的條碼閱讀器是不是也不錯?除了 QR Code 之外,AVFoundation 框架支援以下型式的條碼:
- UPC-E (
AVMetadataObject.ObjectType.upce
) - Code 39 (
AVMetadataObject.ObjectType.code39
) - Code 39 mod 43 (
AVMetadataObject.ObjectType.code39Mod43
) - Code 93 (
AVMetadataObject.ObjectType.code93
) - Code 128 (
AVMetadataObject.ObjectType.code128
) - EAN-8 (
AVMetadataObject.ObjectType.ean8
) - EAN-13 (
AVMetadataObject.ObjectType.ean13
) - Aztec (
AVMetadataObject.ObjectType.aztec
) - PDF417 (
AVMetadataObject.ObjectType.pdf417
) - ITF14 (
AVMetadataObject.ObjectType.itf14
) - Interleaved 2 of 5 codes (
AVMetadataObject.ObjectType.interleaved2of5
) - Data Matrix (
AVMetadataObject.ObjectType.dataMatrix
)
你的任務是調整目前的 Xcode 專案,可以讓這個範例掃描其他型式的條碼。你必須指示 captureMetadataOutput
來識別條碼型態的陣列,而不只是 QR Code而已。
captureMetadataOutput.metadataObjectTypes = [AVMetadataObject.ObjectType.qr]
我將留給你找出解決方案,我們在以下的 Xcode 專案也有提供解答,我鼓勵在繼續往下之前,你能夠自己解決問題,這會比較有趣,而這也是真正了解程式運作的最佳學習方式。
如果你已經盡力了但是還是卡住,你可以至以下網址來下載解答。
本文摘自《iOS 17 App程式設計進階攻略》一書。如果你想繼續閱讀和下載完整程式碼,你可以從AppCoda網站購買完整電子版,全書範例檔皆可下載。