How to Build a QR Code Scanner in SwiftUI

In this tutorial, I will show you how to build a QR code scanner in SwiftUI.

Will start with a simple and basic QR code scanner. There will be no button. On opening the App the camera will open automatically and once it finds a QR code, it will show the value of the QR code as a string. That’s all.

Then we will add copy button and open URL option if any URL found in the QR code.

Note: This is just for tutorial purposes, thus I am not focusing on creating good UI. It is meant just to show how to build QR code scanner in SwiftUI.

I will use AVFoundation to interact with the device’s camera.

So you have to keep in mind this:

  • Your app must request for device’s camera permission in order to access the camera.

To make it possible you have to add a new entry in info.plist

Take a look at Add NSCameraUsageDescription key in info.plist – This tutorial will guide you to add that to your project’s info.plist

If you do not do this, your app will crash. Believe me, it will take less than a minute to add it.

We need to import AVFoundation for adding vibration and sound when a QR code is found.

QR Code scanner in SwiftUI (Open camera and scan QR code)

import SwiftUI
import AVFoundation

struct QRCodeScannerView: UIViewControllerRepresentable {
    class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate {
        var parent: QRCodeScannerView

        init(parent: QRCodeScannerView) {
            self.parent = parent
        }

        func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
            if let metadataObject = metadataObjects.first {
                guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else { return }
                guard let stringValue = readableObject.stringValue else { return }
                AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
                AudioServicesPlaySystemSound(SystemSoundID(1106))
                parent.didFindCode(stringValue)
            }
        }
    }

    var didFindCode: (String) -> Void

    func makeCoordinator() -> Coordinator {
        return Coordinator(parent: self)
    }

    func makeUIViewController(context: Context) -> UIViewController {
        let viewController = UIViewController()
        let session = AVCaptureSession()

        guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else { return viewController }
        let videoInput: AVCaptureDeviceInput

        do {
            videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
        } catch {
            return viewController
        }

        if (session.canAddInput(videoInput)) {
            session.addInput(videoInput)
        } else {
            return viewController
        }

        let metadataOutput = AVCaptureMetadataOutput()

        if (session.canAddOutput(metadataOutput)) {
            session.addOutput(metadataOutput)

            metadataOutput.setMetadataObjectsDelegate(context.coordinator, queue: DispatchQueue.main)
            metadataOutput.metadataObjectTypes = [.qr]
        } else {
            return viewController
        }

        let previewLayer = AVCaptureVideoPreviewLayer(session: session)
        previewLayer.frame = viewController.view.layer.bounds
        previewLayer.videoGravity = .resizeAspectFill
        viewController.view.layer.addSublayer(previewLayer)

        session.startRunning()

        return viewController
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}

struct ContentView: View {
    @State private var scannedCode: String?

    var body: some View {
        VStack {
            if let scannedCode = scannedCode {
                Text("Scanned code: \(scannedCode)")
            } else {
                QRCodeScannerView { code in
                    self.scannedCode = code
                }
                .edgesIgnoringSafeArea(.all)
            }
        }
    }
}

Output:

QR code scanner in SwiftUI

Once the QR code is found, it makes a small beep sound a slight vibration that you can feel.

AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate)) this is for the vibration and

AudioServicesPlaySystemSound(SystemSoundID(1106)) this is for the beep sound.

If you wish you can change the sound. You can check this: Haptic Feedback with Sound in SwiftUI

VStack {
    if let scannedCode = scannedCode {
        Text("Scanned code: \(scannedCode)")
    } else {
        QRCodeScannerView { code in
            self.scannedCode = code
        }
        .edgesIgnoringSafeArea(.all)
    }
}

This is the part of ContentView where I used Text("Scanned code: \(scannedCode)")

You can customize this part to make it creative and better.

You might be thinking that this is too simple that there is no button to start the Scanner or no button to scan again.

Don’t worry, we will add those too.

Adding start scanning and scan again button to our QR code Scanner

In order to add these button, we just need to modify the ContentView and rest of the code will be same.

This is our new updated ContentView struct.

struct ContentView: View {
    @State private var scannedCode: String?
    @State private var isScanning = false

    var body: some View {
        VStack {
            if let scannedCode = scannedCode {
                Text("Scanned code: \(scannedCode)")
                    .padding()
                Button("Scan Again") {
                    self.isScanning = true
                    self.scannedCode = nil
                }
                .padding()
            } else {
                if isScanning {
                    QRCodeScannerView(didFindCode: { code in
                        self.scannedCode = code
                    }, didTapStartScanning: {
                        self.isScanning = true
                    })
                    .edgesIgnoringSafeArea(.all)
                } else {
                    Button("Start Scanning") {
                        self.isScanning = true
                    }
                    .padding()
                }
            }
        }
    }
}

This will run perfectly.

It would be great if we add a copy to the clipboard option to copy our scanned code.

Adding copy to clipboard option to our QR code scanner

Update the ContentView struct like this:

struct ContentView: View {
    @State private var scannedCode: String?
    @State private var isScanning = false

    var body: some View {
        VStack {
            if let scannedCode = scannedCode {
                Text("Scanned code: \(scannedCode)")
                    .padding()
                HStack {
                    Button("Copy") {
                        UIPasteboard.general.string = scannedCode
                    }
                    .padding()
                    Button("Scan Again") {
                        self.isScanning = true
                        self.scannedCode = nil
                    }
                    .padding()
                }
            } else {
                if isScanning {
                    QRCodeScannerView(didFindCode: { code in
                        self.scannedCode = code
                    }, didTapStartScanning: {
                        self.isScanning = true
                    })
                    .edgesIgnoringSafeArea(.all)
                } else {
                    Button("Start Scanning") {
                        self.isScanning = true
                    }
                    .padding()
                }
            }
        }
    }
}

If you intend to scan URL too with QR code:

We often scan QR codes that is actually an URL and we need to open that. So copying the code and pasting in a browser might be a time-consuming task. To make it easier for us, we will add an open URL button if a URL is found.

Update the ContentView part like this:

struct ContentView: View {
    @State private var scannedCode: String?
    @State private var isScanning = false

    var body: some View {
        VStack {
            if let scannedCode = scannedCode {
                Text("Scanned code: \(scannedCode)")
                    .padding()

                if let url = URL(string: scannedCode), UIApplication.shared.canOpenURL(url) {
                    HStack {
                        Button("Open URL") {
                            UIApplication.shared.open(url)
                        }
                        .padding()
                    }
                }

                HStack {
                    Button("Copy") {
                        UIPasteboard.general.string = scannedCode
                    }
                    .padding()
                    Button("Scan Again") {
                        self.isScanning = true
                        self.scannedCode = nil
                    }
                    .padding()
                }
            } else {
                if isScanning {
                    QRCodeScannerView(didFindCode: { code in
                        self.scannedCode = code
                    }, didTapStartScanning: {
                        self.isScanning = true
                    })
                    .edgesIgnoringSafeArea(.all)
                } else {
                    Button("Start Scanning") {
                        self.isScanning = true
                    }
                    .padding()
                }
            }
        }
    }
}

Output:

QR code scanner built in SwiftUI

There are a lot of things you can add with this app. Let everyone know what you want to add in the comment section below.

If you are a beginner, you can check my another tutorial on Create SwiftUI App for Movie Listing using TMDB API

Leave a Reply

Your email address will not be published. Required fields are marked *