Swift 程式語言

如何使用AVFoundation進行QR Code掃描

如何使用AVFoundation進行QR Code掃描
如何使用AVFoundation進行QR Code掃描
In: Swift 程式語言

首先,什麼是QR Code?我相信你們大多數都已經知道什麼是QR Code了。倘若你還沒有聽過,看一下以下的這張圖,這就是QR Code。

QR Code Sample

提示: 你可以自己建立 QR Code,只要去QR Code Monkey網站便可以自己做一個。

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掃描功能吧。

QR Code Scanner

導入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,我們只是還沒察覺到,不過,以下是我們準備要去調整的部分:

  1. 當QR Code被偵測後,App會將QR Code以綠色方框突顯出來。
  2. 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 綠色方框的設定。透過呼叫viewPreviewLayertransformedMetadataObjectForMetadataObject 方法,元資料物件的視覺屬性被轉換為層座標(layer coordinates),也因此我們可以找到QR Code的邊界以建構綠色方框。

let barCodeObject = videoPreviewLayer?.transformedMetadataObjectForMetadataObject(
metadataObj as AVMetadataMachineReadableCodeObject) as AVMetadataMachineReadableCodeObject

qrCodeFrameView?.frame = barCodeObject.bounds

最後,我們將QR Code解碼成為人們可以閱讀的資訊。這非常簡單,解碼的資訊可以由AVMetadataMachineReadableCodeObjectstringValue 屬性來存取。

現在可以準備開始測試了!按下Run按鈕,並在實體裝置中運行。打開後,將它面對QR Code,如下圖所示。此App 會立刻偵測到 QR Code,並進行資訊解碼。

qrcode-demo

你的作業 – 條碼閱讀器

本章範例的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 專案

譯者簡介:王豪勳 -渥合數位服務創辦人,畢業於台灣大學應用力學研究所,曾在半導體產業服務多年,近年來專注於協助客戶進行App軟體以及網站開發,平常致力於研究各式最軟硬體技術,擁有多本譯作。
原文: Building a QR Code Reader in Swift

本文摘自提升iOS 8 App程式設計進階實力的30項關鍵技巧。如有興趣了解其他技巧,可到博客來天瓏購買此書。

作者
Simon Ng
軟體工程師,AppCoda 創辦人。著有《iOS 17 App 程式設計實戰心法》、《iOS 17 App程式設計進階攻略》以及《精通SwiftUI》。曾任職於HSBC, FedEx等跨國企業,專責軟體開發、系統設計。2012年創立AppCoda技術部落格,定期發表iOS程式教學文章。現時專注發展AppCoda業務,致力於iOS程式教學、產品設計及開發。你可以到推特與我聯絡。
評論
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。