Das AVFoundation-Framework ermöglicht es mit überschaubarem Aufwand, einen QR-Code-Scanner auf Basis eines eigenen View-Controllers umzusetzen. Im Zuge eines aktuellen Projekts habe ich genau einen solchen benötigt und – das möchte ich an dieser Stelle nicht verschweigen – eine enorm hilfreiche Vorlage bei Hacking with Swift gefunden (siehe Links am Ende des Artikels). Diese Vorlage habe ich lediglich leicht abgewandelt und um einen zusätzlichen Delegate ergänzt (dazu gleich mehr). An dieser Stelle findet ihr bereits einmal den Code des QRCodeScannerViewController
:
import AVFoundation
import UIKit
class QRCodeScannerViewController: UIViewController {
var captureSession: AVCaptureSession!
var previewLayer: AVCaptureVideoPreviewLayer!
var delegate: QRCodeScannerViewControllerDelegate!
override var prefersStatusBarHidden: Bool {
return true
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .portrait
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.black
captureSession = AVCaptureSession()
guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else { return }
let videoInput: AVCaptureDeviceInput
do {
videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
} catch {
return
}
if (captureSession.canAddInput(videoInput)) {
captureSession.addInput(videoInput)
} else {
captureSession = nil
return
}
let metadataOutput = AVCaptureMetadataOutput()
if (captureSession.canAddOutput(metadataOutput)) {
captureSession.addOutput(metadataOutput)
metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
metadataOutput.metadataObjectTypes = [.qr]
} else {
captureSession = nil
return
}
previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
previewLayer.frame = view.layer.bounds
previewLayer.videoGravity = .resizeAspectFill
view.layer.addSublayer(previewLayer)
captureSession.startRunning()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if (captureSession?.isRunning == false) {
captureSession.startRunning()
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if (captureSession?.isRunning == true) {
captureSession.stopRunning()
}
}
func found(code: String) {
delegate.qrCodeScannerViewControllerDidScanCode(code)
}
}
extension QRCodeScannerViewController: AVCaptureMetadataOutputObjectsDelegate {
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
captureSession.stopRunning()
if let metadataObject = metadataObjects.first {
guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else { return }
guard let stringValue = readableObject.stringValue else { return }
AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
found(code: stringValue)
}
dismiss(animated: true)
}
}
protocol QRCodeScannerViewControllerDelegate {
func qrCodeScannerViewControllerDidScanCode(_ code: String)
}
Sobald die View lädt beziehungsweise erscheint, baut sie eine Session zur Videoaufzeichnung und Erkennung von QR-Codes auf. Der Delegate-Callback des AVCaptureMetadataOutputObjectsDelegate
ist aus Gründen der Übersichtlichkeit in eine separate Extension ausgelagert.
Zusätzlich habe ich einen eigenen Delegate namens QRCodeScannerViewControllerDelegate
eingebunden. Über den lässt sich der gescannte Code über eine passende Delegate-Instanz auslesen, die man nach Erstellung des View-Controllers über dessen delegate
-Property setzt.
Einbindung in SwiftUI
Dieser View-Controller allein reichte mir aber nicht aus, denn eingangs genanntes Projekt basiert auf SwiftUI. Entsprechend wollte ich den QR-Code-Scanner als View in SwiftUI einbinden.
Zu diesem Zweck erzeugte ich eine neue SwiftUI-View namens QRCodeScannerView
, die konform zum UIViewControllerRepresentable
-Protokoll ist. Diese View besitzt einen Status namens scannedCode
in Form eines Bindings. Sobald ein QR-Code erkannt und erfasst wurde, soll der ermittelte Code in dieses Binding geschrieben werden. So lässt sich an beliebiger Stelle innerhalb des App-Projekts ein QR-Code abfragen.
Um den QR-Code aus dem QRCodeScannerViewController
auszulesen, kommt ein Coordinator zum Einsatz, der konform zum von mir selbst implementierten QRCodeScannerViewControllerDelegate
-Protokoll ist. In der makeUIViewController(context:)
-Methode weise ich der erstellten QRCodeScannerViewController
-Instanz jenen Coordinator als Delegate zu.
Feuert der View-Controller nun über seinen Delegate die qrCodeScannerViewControllerDidScanCode(_:)
-Methode, gebe ich den so erhaltenen Code über den Coordinator zurück an die SwiftUI-View, genauer gesagt an die scannedCode
-Property. So schließt sich der Kreis, und der über den View-Controller gescannte Code steht unter SwiftUI zur Verfügung.
import SwiftUI
struct QRCodeScannerView: UIViewControllerRepresentable {
@Binding var scannedCode: String?
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> QRCodeScannerViewController {
let qrCodeScannerViewController = QRCodeScannerViewController()
qrCodeScannerViewController.delegate = context.coordinator
return qrCodeScannerViewController
}
func updateUIViewController(_ uiViewController: QRCodeScannerViewController, context: Context) {}
class Coordinator: NSObject, QRCodeScannerViewControllerDelegate {
var parent: QRCodeScannerView
init(_ qrCodeScannerView: QRCodeScannerView) {
parent = qrCodeScannerView
}
func qrCodeScannerViewControllerDidScanCode(_ code: String) {
parent.scannedCode = code
}
}
}
Einsatz in SwiftUI
Die QRCodeScannerView
könnt ihr nun nach Belieben einsetzen, um QR-Codes via SwiftUI zu erfassen. Bedenkt aber, in der Info.plist-Datei eures Projekts den Key Privacy – Camera Usage Description zu ergänzen. Über den müsst ihr in Form eines Strings festlegen, warum ihr mit eurer App auf die Kamera zugreifen wollt. Fehlt der Schlüssel, kommt es beim Erstellen der QRCodeScannerView
zum Crash.
Ich selbst nutzte einen eigens kreierten ScanButton
, um die QRCodeScannerView
in Form eines Sheets einzublenden. Über diesen Button ermittle ich bei dessen Betätigung auch, ob die App die Berechtigung zum Zugriff auf die Kamera besitzt. Falls nicht, blende ich einen Alert ein, der auf dieses Problem hinweist und den Nutzer auf Wunsch direkt in die Einstellungen führt, wo er die Berechtigungen ändern kann.
struct ScanButton: View {
@State private var showsScanner = false
@State private var showsMissingCameraAccessAlert = false
@Binding var scannedCode: String?
var body: some View {
Button(action: {
AVCaptureDevice.requestAccess(for: .video, completionHandler: { (granted: Bool) -> Void in
if granted {
showsScanner = true
} else {
showsMissingCameraAccessAlert = true
}
})
}, label: {
VStack {
Image(systemName: "qrcode.viewfinder")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 80)
Text("Scannen")
.font(.title)
}
})
.alert("Die App bentötigt für das Scannen des QR-Codes Zugriff auf die Kamera", isPresented: $showsMissingCameraAccessAlert, actions: {
openSettingsButton
dismissMissingCameraAccessAlertButton
}, message: {
Text("Bitte erlauben Sie den Zugriff auf die Kamera in den Einstellungen.")
})
.sheet(isPresented: $showsScanner) {
QRCodeScannerView(scannedCode: $scannedCode)
}
}
private var openSettingsButton: some View {
Button("Einstellungen öffnen") {
showsMissingCameraAccessAlert = false
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
}
}
private var dismissMissingCameraAccessAlertButton: some View {
Button("Abbrechen", role: .cancel) {
showsMissingCameraAccessAlert = false
}
}
}
Fazit
Ich war überrascht, wie unkompliziert sich ein QR-Code-Scanner in SwiftUI integrieren ließ. Hier zeigt sich wieder einmal ganz deutlich, wie mächtig die verschiedenen Representable-Protokolle des SwiftUI-Frameworks sind. Der Coordinator kümmert sich um die essenziellen Aktionen zum Verarbeiten der Daten und gibt diese an ein Binding weiter. Wo genau der Status dieses Bindings gespeichert ist, ist irrelevant, wodurch sich die QRCodeScannerView
an beliebigen Stellen einsetzen lässt.
Euer Thomas
Weiterführende Links zum Artikel
- Hacking with Swift: How to scan a QR code: https://www.hackingwithswift.com/example-code/media/how-to-scan-a-qr-code
Schreibe einen Kommentar