How to Make a QR or Barcode Scanner in SwiftUI

Chase
5 min readFeb 10, 2023

--

With iOS 16 and Apples release of the DataScanner API, building a QR or barcode scanner has never been easier! In this tutorial, you will learn how to use the DataScannerViewController, how to use UIViewControllerRepresentable to display code from UIKit in SwiftUI, and how to pass data between the two frameworks, all while writing a minimal amount of code.

How to use a UIKit View Controller in SwiftUI

This is the first step of getting all the code that Apple has already written for us into SwiftUI. I personally prefer to wrap the UIKit APIs in their own struct so that the call site in SwiftUI is clean and simple.

In our Xcode project, lets add a new Swift file, and give it the name “DataScannerRepresentable”. Inside that new file, we will paste the following code (don’t worry, we will go over the code after this).

// DataScannerRepresentable.swift
import SwiftUI
import VisionKit

struct DataScannerRepresentable: UIViewControllerRepresentable {
@Binding var shouldStartScanning: Bool
@Binding var scannedText: String
var dataToScanFor: Set<DataScannerViewController.RecognizedDataType>

class Coordinator: NSObject, DataScannerViewControllerDelegate {
var parent: DataScannerRepresentable

init(_ parent: DataScannerRepresentable) {
self.parent = parent
}

func dataScanner(_ dataScanner: DataScannerViewController, didTapOn item: RecognizedItem) {
switch item {
case .text(let text):
parent.scannedText = text.transcript
case .barcode(let barcode):
parent.scannedText = barcode.payloadStringValue ?? "Unable to decode the scanned code"
default:
print("unexpected item")
}
}
}

func makeUIViewController(context: Context) -> DataScannerViewController {
let dataScannerVC = DataScannerViewController(
recognizedDataTypes: dataToScanFor,
qualityLevel: .accurate,
recognizesMultipleItems: true,
isHighFrameRateTrackingEnabled: true,
isPinchToZoomEnabled: true,
isGuidanceEnabled: true,
isHighlightingEnabled: true
)

dataScannerVC.delegate = context.coordinator

return dataScannerVC
}

func updateUIViewController(_ uiViewController: DataScannerViewController, context: Context) {
if shouldStartScanning {
try? uiViewController.startScanning()
} else {
uiViewController.stopScanning()
}
}

func makeCoordinator() -> Coordinator {
Coordinator(self)
}
}

In the code above, we need to import SwiftUI because we are using the “@Binding” properties. We import VisionKit because it gives us the Data Scanner API.

The Bindings are the parameters that allow us to pass data between our other SwiftUI views and our UIViewRepresentable.

The “class Coordinator” section is where the data is returned from our scanner, either text, a barcode, or a fail safe default value.

The makeUIViewController function is where we are creating the UIKit view that will be use in our SwiftUI view later. It is also where we get to see all the cool features of a code scanner that Apple gives us for free. For example, the recognizedDataTypes allows us to scan for a specific type of code like a qr, ean8, pdf417, and many more. It will even give us the ability to scan for text. You can also enable/disable pinch to zoom, have it highlight the recognized code types in the camera preview, or even have it overlay helpful tips for the user right on screen!

The updateUIViewController function tells the view controller when it should start scanning.

The makeCoordinator function is the communication bridge between SwiftUI and UIKit that allows them to coordinate with each other.

With that, the scanner has been built, and the UIKit side of the project is done.

Camera Usage Description

Before we can use our DataScanner in the app, we have to add a camera usage description to our info.plist file.

THIS STEP IS VITAL — WITHOUT IT, OUR APP WON’T WORK.

If we are going to release the app to the store, our actual description of why our app needs access to the camera is very important. This should describe to users why your app needs access to the camera, and/or what your app intends to use the data for. One good way to come up with the description is to ask yourself, “If I was a brand new user to this app, why would I give them access to my camera?”

If we are just testing out the code, it is still a good idea to use a real camera usage description. If you decide to submit this to the App Store later, it’s easy to forget to update this one piece of text. And Apple may reject your app if the description is not good enough.

That being said, I ran out of room for the screenshot below, and the (goodReasonHere) should be replaced with something like “building a demo app to teach others how to use the barcode scanner.” Do not include the parenthesis in your description.

A screenshot of the info.plist file that has the camera usage description highlighted

Displaying the scanner in SwiftUI

Here is where all of the hard work from earlier in the tutorial pays off.

// ContentView.swift
import SwiftUI
import VisionKit

struct ContentView: View {
@State var isShowingScanner = true
@State private var scannedText = ""

var body: some View {
if DataScannerViewController.isSupported && DataScannerViewController.isAvailable {
ZStack(alignment: .bottom) {
DataScannerRepresentable(
shouldStartScanning: $isShowingScanner,
scannedText: $scannedText,
dataToScanFor: [.barcode(symbologies: [.qr])]
)

Text(scannedText)
.padding()
.background(Color.white)
.foregroundColor(.black)
}
} else if !DataScannerViewController.isSupported {
Text("It looks like this device doesn't support the DataScannerViewController")
} else {
Text("It appears your camera may not be available")
}
}
}

In our view where we want to display scanner, we want to again import VisionKit so that we can check to see if the DataScanner is available and supported by our device.

In the case where the scanner is supported and is available, we are using a ZStack (or depth stack) to overlay the scanned text on top of the scanner. By telling the scanner that we are looking for an specific barcode set “.barcode(symbologies: [.qr])”, the scanner will only search for qr codes. If we wanted to find any barcodes (including qr), we would change this to be “.barcode()”.

In order for us to get the data from a barcode, all we need to do is run the app, point the camera at a qr code, tap on a qr code that is highlighted on the screen, and the data from the code will be displayed at the bottom of our screen. In a real app, we would probably prefer to display the text content in its own view, or possibly another screen. However, since this is an example app, we are overlaying the results from the scanner on top of the scanner image.

We also want to make sure to give the users a good experience (even for our example). This is why we include the if/else statements to conditionally show the view. In a real app, we would probably want to build a view for each case of the if check, that would give the user more information about how they may be able to fix the issues (ie. try this app on a different device, allow access to the camera in the settings app, etc.).

If you have any questions on the topic, or know of another way to accomplish the same task, feel free to respond to the post, or share it with a friend, and get their opinion on it.

Thank you for taking the time to make it this far. Have a great day!

--

--