SwiftUI 框架

在 SwiftUI 開發一個 QR Code 讀取器 App

QR Code 近年在消費市場日漸普及,相信很多 iOS 開發者都想為自己的 App 添加讀取 QR Code 的功能。在這篇文章中,我會使用這個新的 SwiftUI 框架,帶大家來實作一個 QR Code 讀取器 App。
在 SwiftUI 開發一個 QR Code 讀取器 App
Photo by David Dvořáček on Unsplash
In: SwiftUI 框架

相信大家都知道什麼是 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)顯示在畫面底部。

QR Code Reader app for demo purpose

現在,大家都了解範例 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 的讀取以影片擷取為基礎。要執行即時的擷取,我們只需要:

  1. 查詢後置鏡頭裝置。
  2. 設定 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 結構。我們只需要初始化 ContentViewbody

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

info plist for SwiftUI project

如果我們現在執行 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。

譯者簡介:Kelly Chan-AppCoda 編輯小姐。

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