相信大家都知道什麼是 QR Code,如果大家真的不知道,可以看看上面的圖片,那就是一個 QR Code。
QR(Quick Response 的縮寫)Code 是由 Denso 開發的一個二維條碼。QR Code 原來的用途是為製造業追蹤零件,近年在消費市場日漸普及,主要是將 URL 編碼為網頁導引或提供市場資訊用。與大家熟悉的條碼 (barcode) 不同,QR Code 包含了水平與垂直方向的資訊,因此它能夠以數字和文字儲存更多資料。我不會在這篇文章深入討論 QR Code 技術上的細節。如你有興趣了解更多,可以參閱 QR code 的官方網站。
各位 iOS 開發者應該都讓自己的 App 讀取 QR Code,我之前就寫了一篇教學文章,教大家使用 UIKit 和 AVFoundation 構建 Qr Code 讀取器。在 SwiftUI 發佈之後,讓我們來使用這個新的 UI 框架,實作一個一樣的 QR Code 讀取器 App 吧!
QR Code 讀取器 App 的運作模式
我們準備要構建的範例 App 非常簡單。在開始實作之前,大家需要記住一個重點,任何在 iOS 上的條碼掃描,包括 QR Code 在內,全部都是以影片擷取為基礎。記住這個概念,這會有助你了解整篇教學的內容。
那麼,範例 App 是如何運作的呢?
下面的螢幕截圖,是這個 App UI 的樣貌。這個 App 運作起來與影片擷取 App 非常相像,只是沒有錄製的功能。當 App 開啟後,就會利用 iPhone 後置鏡頭來自動辨識及解碼 QR Code,並把解碼後的資訊(比如說是一個 URL)顯示在畫面底部。
現在,大家都了解範例 App 的運作模式了。讓我們開始在 SwiftUI 開發 QR Code 讀取器 App 吧!
建立 QRScannerController 類別
SwiftUI 框架沒有啟動相機的內置 API,如果我們需要使用裝置的相機,就要利用 UIKit 來建立一個視圖控制器來擷取影片。之後,我們再使用 UIViewControllerRepresentable
,把視圖控制器添加到 SwiftUI 專案。
在 Xcode 建立一個新的 SwiftUI 專案後,建立一個名為 QRScanner.swift
的 Swift 檔案,並在檔案中匯入 SwiftUI 和 AVFoundation 框架:
import SwiftUI
import AVFoundation
接著,實作一個新的類別 QRScannerController
:
class QRScannerController: UIViewController {
var captureSession = AVCaptureSession()
var videoPreviewLayer: AVCaptureVideoPreviewLayer?
var qrCodeFrameView: UIView?
var delegate: AVCaptureMetadataOutputObjectsDelegate?
override func viewDidLoad() {
super.viewDidLoad()
// Get the back-facing camera for capturing videos
guard let captureDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else {
print("Failed to get the camera device")
return
}
let videoInput: AVCaptureDeviceInput
do {
// Get an instance of the AVCaptureDeviceInput class using the previous device object.
videoInput = try AVCaptureDeviceInput(device: captureDevice)
} catch {
// If any error occurs, simply print it out and don't continue any more.
print(error)
return
}
// Set the input device on the capture session.
captureSession.addInput(videoInput)
// Initialize a AVCaptureMetadataOutput object and set it as the output device to the capture session.
let captureMetadataOutput = AVCaptureMetadataOutput()
captureSession.addOutput(captureMetadataOutput)
// Set delegate and use the default dispatch queue to execute the call back
captureMetadataOutput.setMetadataObjectsDelegate(delegate, queue: DispatchQueue.main)
captureMetadataOutput.metadataObjectTypes = [ .qr ]
// Initialize the video preview layer and add it as a sublayer to the viewPreview view's layer.
videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
videoPreviewLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill
videoPreviewLayer?.frame = view.layer.bounds
view.layer.addSublayer(videoPreviewLayer!)
// Start video capture.
DispatchQueue.global(qos: .background).async {
self.captureSession.startRunning()
}
}
}
如果你有讀過之前那篇教學文章,應該會明白上面的程式碼。不過,讓我簡單說明一下。如前文所述,QR Code 的讀取以影片擷取為基礎。要執行即時的擷取,我們只需要:
- 查詢後置鏡頭裝置。
- 設定
AVCaptureSession
物件的輸入給對應的AVCaptureDevice
來擷取影片。
因此,我們在 viewDidLoad
方法中使用 AVCaptureDevice
來初始化後置鏡頭。接著,使用相機裝置創建一個 AVCaptureDeviceInput
的實例,然後輸入裝置就會被添加到 captureSession
物件。如此一來,就會建立出一個 AVCaptureMetadataOutput
實例為 capture session 的輸出,並添加到同一個 session 物件中。
我們也設置了委派物件 (AVCaptureMetadataOutputObjectsDelegate
) 來處理 QR Code。App 從接受器中擷取到 QR Code 後,就會把 QR Code 交給委派物件。我們會在文章之後的部分實作這個委派物件。
我們可以利用 metadataObjectTypes
屬性,來指定我們對哪種元資料感興趣;而 .qr
的值就代表我們想要進行 QR code 掃描。
最後的幾行程式碼就是用來建立影片預覽層 (preview layer),並加入為 viewPreview 視圖的子層 (sublayer)。這樣我們就可以透過裝置的相機,將擷取的影片顯示在畫面上。
整合 QRScannerController 到 SwiftUI
現在,用來擷取影片和掃描 QR code 的視圖控制器已經準備好了,我們如何把它整合到 SwiftUI 專案呢?SwiftUI 有一個 UIViewControllerRepresentable
協定,可以建立和管理 UIViewController
物件。
在同一個檔案中,讓我們建立一個符合 UIViewControllerRepresentable
協定的 QRScanner
結構:
struct QRScanner: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> QRScannerController {
let controller = QRScannerController()
return controller
}
func updateUIViewController(_ uiViewController: QRScannerController, context: Context) {
}
}
我們實作了 UIViewControllerRepresentable
協定所需的方法。在 makeUIViewController
方法中,我們回傳了 QRScannerController
的實例。因為我們不需要更新視圖控制器的狀態,所以 updateUIViewController
是空白的。
這樣我們就可以在 SwiftUI 專案中使用 UIViewController
物件了。
使用 QRScanner
現在,讓我們到 ContentView.swift
使用剛剛建立的 QRScanner
結構。我們只需要初始化 ContentView
的 body
:
struct ContentView: View {
@State var scanResult = "No QR code detected"
var body: some View {
ZStack(alignment: .bottom) {
QRScanner()
Text(scanResult)
.padding()
.background(.black)
.foregroundColor(.white)
.padding(.bottom)
}
}
}
另外,我也添加了一個文本標籤 (text label) 來顯示 QR Code 掃描的結果。在模擬器上,App 應該只會顯示文本標籤。之後我們在實體裝置 (iPhone/iPad) 中執行 App,App 就會開啟內建的相機。
要成功啟動 App,我們需要在 Info.plist
檔案中添加一個名為 NSCameraUsageDescription
的 key。在專案導覽器中,選擇我們的專案並點擊 Info,讓我們加入一個新列,並把 key 設定為 Privacy - Camera Usage Description,其值設為 We need to access your camera for scanning QR code 。
如果我們現在執行 App,它會自動存取內建的相機,並開始擷取影片。不過,App 還沒可以掃描 QR Code。
處理掃描結果
在 ContentView
中,有一個用來儲存掃描結果的狀態變數 (state variable)。問題是,QRScanner
(或 QRScannerController
)如何把解碼 QR Code 後的資訊回傳給 ContentView
呢?
不知道你記不記得,我們還沒有實作用來處理 QR Code 的委派(即 AVCaptureMetadataOutputObjectsDelegate
的實例)。我們需要實作以下的 AVCaptureMetadataOutputObjectsDelegate
的委派方法:
optional func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection)
這個委派是用來檢索解碼資訊,並回傳給 SwiftUI App。我們需要提供一個也是採用 AVCaptureMetadataOutputObjectsDelegate
協定 Coordinator
實例,來處理在視圖控制器物件和 SwiftUI 界面之間的交互,讓它們可以交換數據。
首先,讓我們在 QRScanner
宣告一個 binding:
@Binding var result: String
接著,在 QRScanner
添加以下程式碼,來設定 Coordinator
類別:
class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate {
@Binding var scanResult: String
init(_ scanResult: Binding<String>) {
self._scanResult = scanResult
}
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
// Check if the metadataObjects array is not nil and it contains at least one object.
if metadataObjects.count == 0 {
scanResult = "No QR code detected"
return
}
// Get the metadata object.
let metadataObj = metadataObjects[0] as! AVMetadataMachineReadableCodeObject
if metadataObj.type == AVMetadataObject.ObjectType.qr,
let result = metadataObj.stringValue {
scanResult = result
print(scanResult)
}
}
}
這個類別有一個 binding 來更新掃描結果,如此一來,我們就可以把掃描結果回傳到 SwiftUI 物件。
要處理掃描 QR Code 的結果,我們還需要實作 metadataOutput
方法。方法的第二個參數(也就是 metadataObjects
)是一個陣列物件,當中包含了所有已經讀取的元資料物件。我們最先要做的事,就是確認這個陣列不是 nil
,它最少要包含一個物件。否則,我們就把 scanResult
設置為 No QR code detected。
如果有發現到元資料物件,我們就要檢查物件是否 QR Code,並解碼當中的資訊。我們可以使用 AVMetadataMachineReadableCode
物件的 stringValue
屬性,來存取解碼後的資訊。
準備好 Coordinator
類別後,讓我們插入以下方法,把 Coordinator
實例添加到 QRScanner
:
func makeCoordinator() -> Coordinator {
Coordinator($result)
}
另外,讓我們這樣更新 makeUIViewController
方法,來把 coordinator
物件分配給控制器的 delegate
:
func makeUIViewController(context: Context) -> QRScannerController {
let controller = QRScannerController()
controller.delegate = context.coordinator
return controller
}
專案差不多完成了!現在回到 ContentView.swift
,並如此更新 QRScanner()
來傳遞掃描結果:
QRScanner(result: $scanResult)
完成了!我們可以按下 Run 按鈕,並在實體裝置中測試這個 App。