[2024 鐵人賽] Day 23: 掃描發票 QRCode 與取得內容
![[2024 鐵人賽] Day 23: 掃描發票 QRCode 與取得內容](https://ooorito.com/wp-content/uploads/2024/10/Day23.webp)
在今天的挑戰中,我們要加入一個重要的功能,就是掃描發票 QRCode 來幫助使用者加入家用品。這個功能會幫助使用者能夠更快速、方便的加入所購買的家用品。雖然今天的目標只集中在掃描和取得內容,但這將為接下來的工作打下基礎。
目標
- 在
AddItemView中新增一個相機按鈕,點擊後開啟相機。 - 實作掃描 QRCode 功能並取得內容。
- 掃描前可開啟取得權限的畫面。
主要實作
在 AddItemView 中新增掃描按鈕
在新增頁面的右上角新增一個相機按鈕,讓使用者可以點擊開啟相機進行 QRCode 掃描。
struct AddItemView: View {
@ObservedObject var viewModel: AddItemViewModel
@State private var scanResult: String = "No QR code detected" // 儲存掃描結果
var body: some View {
VStack {
//...中間略
.navigationBarItems(trailing: NavigationLink(destination: QRScannerView(result: $scanResult)) {
Image(systemName: "camera")
.font(.title2)
})
}
}
}這段程式碼新增了一個 NavigationLink,它會打開我們稍後實作的 QRScannerView。

實作 QRScannerView
接著我們需要建立一個 QRScannerView,使用 UIViewControllerRepresentable 來將 UIViewController 包裝成 SwiftUI 可以使用的 View。
UIViewControllerRepresentable
UIViewControllerRepresentable 是 SwiftUI 中的一個協定,用來將 UIKit 的 UIViewController 轉換成 SwiftUI 的 View,使我們可以在 SwiftUI 中使用現有的或自定義的 UIViewController。它允許我們在 SwiftUI 的架構中引入 UIKit 的功能,像是使用相機、地圖、或是其他 UIKit 的 Controller。
參考資料:
struct QRScannerView: UIViewControllerRepresentable {
@Binding var result: String // 掃描結果
func makeUIViewController(context: Context) -> QRScannerController {
let scannerController = QRScannerController()
scannerController.delegate = context.coordinator // 設置 delegate 處理掃描結果
return scannerController
}
func updateUIViewController(_ uiViewController: QRScannerController, context: Context) {
// 無需更新
}
func makeCoordinator() -> Coordinator {
Coordinator($result)
}
}這裡使用 UIViewControllerRepresentable 來將 QRScannerController 引入到 SwiftUI 畫面中,並透過 Coordinator 來處理掃描結果。
建立 Coordinator
Coordinator 負責接收來自相機掃描的結果,並將結果傳回 SwiftUI。
在 SwiftUI 中,Coordinator 的作用是充當橋樑,讓 UIKit 的控制器(例如相機)與 SwiftUI 進行溝通。由於 SwiftUI 和 UIKit 是兩個不同的框架,處理交互事件時,我們需要一個協調者來處理 SwiftUI 和 UIKit 之間的資料傳遞,這就是 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) {
if let metadataObject = metadataObjects.first as? AVMetadataMachineReadableCodeObject, metadataObject.type == .qr {
if let stringValue = metadataObject.stringValue {
scanResult = stringValue // 更新掃描結果
}
}
}
}這段程式負責當掃描到 QRCode 後,將內容更新 scanResult。
設定相機權限 (Info.plist)
在 iOS App 中,使用相機等個人資訊時,必須向使用者請求相對應的權限。在這次我們的目標中,掃描 QRCode 需要用到相機,因此我們必須在 Info.plist 中新增相機使用權限的說明。
步驟:
- 打開
Info.plist文件:在 Xcode 左邊的列表中,找到並選擇Info.plist文件。 - 新增相機使用描述:
- 在
Info.plist中,新增一個新的 keyNSCameraUsageDescription。 - 對應的值必須提供需要使用者的相機使用原因說明。例如:
"需要使用相機來掃描 QRCode"。
- 在
Info.plist 範例:
<key>NSCameraUsageDescription</key>
<string>需要使用相機來掃描 QRCode</string>NSCameraUsageDescription:這是 Apple 要求的關鍵,用來解釋為什麼應用需要使用相機。在 App 運行時,當我們第一次嘗試開啟相機時,iOS 會彈出一個對話框,顯示這個描述,讓使用者了解這一權限的用途。
實作 QRScannerController
QRScannerController 是一個 UIViewController,負責顯示相機並進行 QRCode 掃描。當掃描到 QRCode 後,它會將結果傳回 SwiftUI。
建立 QRScannerController 類別
首先需要建立一個自定義的 UIViewController 來管理相機的掃描功能。
class QRScannerController: UIViewController, AVCaptureMetadataOutputObjectsDelegate {
var captureSession = AVCaptureSession()
var videoPreviewLayer: AVCaptureVideoPreviewLayer?
var qrCodeFrameView: UIView?
var onQRCodeScanned: ((String) -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
checkCameraAuthorization()
}
}AVCaptureSession:用於捕捉視訊及音訊,協調視訊及音訊的輸入及輸出。AVCaptureVideoPreviewLayer:用來在螢幕上顯示相機畫面的圖層。這個圖層將相機的輸入影像實時顯示出來,讓用戶可以看到他們正在掃描什麼。AVCaptureMetadataOutputObjectsDelegate:用來捕捉並輸出資料的方法。
參考資料:QRCode掃起來!
檢查相機權限
我們要先取得相機的使用權限,因此需要加入檢查和請求相機使用權限的邏輯。
func checkCameraAuthorization() {
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
// 已授權,開始配置相機
setupCamera()
case .notDetermined:
// 尚未決定,請求授權
AVCaptureDevice.requestAccess(for: .video) { granted in
DispatchQueue.main.async {
if granted {
self.setupCamera()
} else {
self.showPermissionAlert() // 顯示權限不足提示
}
}
}
case .denied, .restricted:
// 已被拒絕或限制,顯示提示
showPermissionAlert()
@unknown default:
fatalError("Unexpected case for camera permission.")
}
}- 檢查權限:
AVCaptureDevice.authorizationStatus(for: .video)檢查 App 對相機的權限狀態。 - 請求權限:如果權限未決定 (
notDetermined),請求相機權限並在使用者決定後進行相對應操作。 - 顯示提示:當權限被拒絕時,我們會顯示一個對話框來告知用戶需要打開權限。
設定相機
已經取得相機權限後,可以開始設定相機的輸入和輸出,並將相機畫面顯示在螢幕上。
func setupCamera() {
captureSession = AVCaptureSession()
guard let captureDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else {
print("無法取得相機裝置")
return
}
do {
let input = try AVCaptureDeviceInput(device: captureDevice)
captureSession.addInput(input)
let captureMetadataOutput = AVCaptureMetadataOutput()
captureSession.addOutput(captureMetadataOutput)
// 設置 delegate 以監聽 QRCode 掃描結果
captureMetadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
captureMetadataOutput.metadataObjectTypes = [.qr]
videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
videoPreviewLayer?.videoGravity = .resizeAspectFill
videoPreviewLayer?.frame = view.layer.bounds
view.layer.addSublayer(videoPreviewLayer!)
DispatchQueue.global(qos: .background).async {
self.captureSession.startRunning()
}
} catch {
print("Error occurred while setting up camera: \(error)")
}
}AVCaptureDeviceInput:這將相機作為輸入設備加入到captureSession中。AVCaptureMetadataOutput:我們使用這個來監聽 QRCode 掃描結果,並將QRCode類型加入到metadataObjectTypes中。AVCaptureVideoPreviewLayer:這一部分負責將相機的畫面顯示在view上。
處理 QRCode 掃描結果
當掃描到 QRCode 時,會觸發回調方法,我們可以在這裡處理掃描結果,並且提供使用者震動反饋。
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
if let metadataObject = metadataObjects.first {
guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject,
let stringValue = readableObject.stringValue else { return }
AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
qrCodeFrameView?.frame = videoPreviewLayer?.transformedMetadataObject(for: metadataObject)?.bounds ?? .zero
onQRCodeScanned?(stringValue)
}
}- 震動反饋:當成功掃描到 QRCode 時,我們通過
AudioServicesPlaySystemSound讓設備震動,告訴用戶掃描成功。
顯示權限不足的提示
如果使用者拒絕相機權限,我們需要提醒他去設定中打開權限。
func showPermissionAlert() {
let alert = UIAlertController(title: "相機權限不足", message: "請到設置中開啟相機權限", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "確定", style: .default))
present(alert, animated: true)
}有點長,這邊提供完整程式碼:
class QRScannerController: UIViewController, AVCaptureMetadataOutputObjectsDelegate {
var captureSession = AVCaptureSession()
var videoPreviewLayer: AVCaptureVideoPreviewLayer?
var qrCodeFrameView: UIView?
var onQRCodeScanned: ((String) -> Void)?
var delegate: AVCaptureMetadataOutputObjectsDelegate?
override func viewDidLoad() {
super.viewDidLoad()
checkCameraAuthorization()
}
// 掃描 QRCode 後觸發
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
if let metadataObject = metadataObjects.first as? AVMetadataMachineReadableCodeObject, let stringValue = metadataObject.stringValue {
print("QR Code: \(stringValue)") // Debug 用
}
}
func checkCameraAuthorization() {
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
setupCamera() // 已授權,開始配置相機
case .notDetermined:
AVCaptureDevice.requestAccess(for: .video) { granted in
if granted {
DispatchQueue.main.async {
self.setupCamera()
}
}
}
default:
print("未授權使用相機")
}
}
func setupCamera() {
guard let captureDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else {
print("無法取得相機裝置")
return
}
do {
let input = try AVCaptureDeviceInput(device: captureDevice)
captureSession.addInput(input)
let captureMetadataOutput = AVCaptureMetadataOutput()
captureSession.addOutput(captureMetadataOutput)
captureMetadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
captureMetadataOutput.metadataObjectTypes = [.qr]
videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
videoPreviewLayer?.videoGravity = .resizeAspectFill
videoPreviewLayer?.frame = view.layer.bounds
view.layer.addSublayer(videoPreviewLayer!)
captureSession.startRunning()
} catch {
print("相機初始化失敗: \(error)")
}
}
}QRScannerController 是一個相機的 Controller,使用 AVCaptureSession 來配置相機,並處理 QRCode 掃描結果。
總結
今天練習到如何在 SwiftUI 使用 UIKit,沒有想到這兩樣東西要連結起來這麼麻煩的事情!這次實作 QRCode 掃描功能,包括在 AddItemView 中新增一個相機按鈕,開啟 QRCode 掃描畫面,並取得 QRCode 中的資料。明天我們再接續解析資料,製作 QRCode 掃描加入家用品功能吧!



