Etiqueta: AVFoundation

  • Real-Time Speed Limit Detection in iOS Using Vision

    Real-Time Speed Limit Detection in iOS Using Vision

    iOS development focused on detecting text in a video recording scene using Vision and AVFoundation is incredibly valuable for developers interested in building apps with real-time image processing or OCR capabilities. This post provides hands-on guidance on how to combine AVFoundation’s video capture features with Vision’s powerful text recognition capabilities, allowing developers to create apps that automatically extract and analyze text from videos. It is especially useful for building innovative apps in fields such as accessibility, document scanning, or interactive media, offering both technical insights and practical code examples to help developers implement advanced text detection in their projects.

    In this post, we will walk through creating a sample iOS app that detects speed limit traffic signs. At the end of the post, you will find a GitHub repository link to the source code.

    iOS Speed signal detection app

    The app basically detects and filter those ones that could fit with a speed limit:

    The View

    Main ContentView retrieves possible speed detection text and just prints a speed limit traffic signal with detected speed value on it:

    struct ContentView: View {
        @State private var detectedSpeed: String = ""
        var body: some View {
            ZStack {
                CameraView(detectedSpeed: $detectedSpeed)
                    .edgesIgnoringSafeArea(.all)
                
                trafficLimitSpeedSignalView(detectedSpeed)
            }
        }
        
        func trafficLimitSpeedSignalView(_ detectedText: String ) -> some View {
            if !detectedText.isEmpty {
                return AnyView(
                ZStack {
                     Circle()
                         .stroke(Color.red, lineWidth: 15)
                         .frame(width: 150, height: 150)
                     Circle()
                         .fill(Color.white)
                         .frame(width: 140, height: 140)
                     
                     Text("\(detectedText)")
                         .font(.system(size: 80, weight: .heavy))
                         .foregroundColor(.black)
                 }
                )
            } else {
                return AnyView(EmptyView())
            }
        }
    }

    CameraView, where all magic takes place…

    Key library frameworks have been the following:

    • AVCaptureSession: Captures video data from the device camera.
    • VNRecognizeTextRequest: Part of Apple’s Vision framework used for Optical Character Recognition (OCR) to recognize text in images.
     
    This code defines a SwiftUI CameraView component that uses the device’s camera to capture video, and processes the video feed to detect and extract speed values from any visible text (e.g., road signs with speed limits).
    struct CameraView: UIViewControllerRepresentable {
        @Binding var detectedSpeed: String
    • UIViewControllerRepresentable structure integrates a UIViewController (specifically a camera view) into a SwiftUI-based application. SwiftUI is future, but not for this applications context yet.
    • @Binding var detectedSpeed: String: A binding to a string that will hold the detected speed from the camera feed. Changes on this property wraper will update main ContentView.
       func makeCoordinator() -> Coordinator {
            return Coordinator(detectedSpeed: $detectedSpeed)
        }
    
        func makeUIViewController(context: Context) -> UIViewController {
            let controller = UIViewController()
            let captureSession = AVCaptureSession()
            captureSession.sessionPreset = .high
    
            guard let videoDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back),
                let videoInput = try? AVCaptureDeviceInput(device: videoDevice) else {
                return controller
            }
            captureSession.addInput(videoInput)
    
            let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
            previewLayer.videoGravity = .resizeAspectFill
            previewLayer.frame = controller.view.layer.bounds
            controller.view.layer.addSublayer(previewLayer)
    
            let videoOutput = AVCaptureVideoDataOutput()
            videoOutput.setSampleBufferDelegate(context.coordinator, queue: DispatchQueue(label: "videoQueue"))
            captureSession.addOutput(videoOutput)
    
            Task { @GlobalManager in
                captureSession.startRunning()
            }
    
            return controller
        }
    
        func updateUIViewController(_ uiViewController: UIViewController, context: Context) { }

    Next, Methods in CameraView:

    • makeCoordinator(): Creates an instance of the Coordinator class that manages the camera’s data output and processes the video feed.

    • makeUIViewController(context:): This method sets up and configures the camera session:

      • AVCaptureSession: A session to manage the input from the camera and output to process the captured video.
      • AVCaptureDevice: Selects the device’s rear camera (.back).
      • AVCaptureDeviceInput: Creates an input from the rear camera.
      • AVCaptureVideoPreviewLayer: Displays a live preview of the video feed on the screen.
      • AVCaptureVideoDataOutput: Captures video frames for processing by the Coordinator class.
      • captureSession.startRunning(): Starts the video capture.
    • updateUIViewController(_, context:): This method is required by the UIViewControllerRepresentable protocol but is left empty in this case because no updates to the view controller are needed after initial setup.

    class Coordinator: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {
    
            @Binding var detectedSpeed: String
    
            init(detectedSpeed: Binding<String>) {
                _detectedSpeed = detectedSpeed
            }
    
            func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
                guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
                recognizeText(in: pixelBuffer)
            }
    
            private func recognizeText(in image: CVPixelBuffer) {
                let textRequest = VNRecognizeTextRequest { [weak self] (request, error) in
                    guard let self,
                        let observations = request.results as? [VNRecognizedTextObservation],
                        let topCandidate = self.getCandidate(from: observations) else {
                        return
                    }
                    if let speedCandidate = Int(topCandidate),
                        (10...130).contains(speedCandidate),
                        speedCandidate % 10 == 0 {
                        print("Speed candidate: \(speedCandidate)")
                        detectedSpeed = "\(speedCandidate)"
                    }
                }
    
                let requestHandler = VNImageRequestHandler(cvPixelBuffer: image, options: [:])
                try? requestHandler.perform([textRequest])
            }
    
            private func getCandidate(from observations: [VNRecognizedTextObservation]) -> String? {
                var candidates = [String]()
                for observation in observations {
                    for candidate in observation.topCandidates(10) {
                        if candidate.confidence > 0.9,
                            let speedCandidate = Int(candidate.string),
                            (10...130).contains(speedCandidate),
                            speedCandidate % 10 == 0 {
                            candidates.append(candidate.string)
                        }
                    }
                }
                return candidates.firstMostCommonItemRepeated()
            }
        }

    Coordinator class is responsible for handling the video feed and extracting relevant text (i.e., speed values) from the camera image.

    • Properties:
      • @Binding var detectedSpeed: String: A binding to update the detected speed from the camera feed.
    • Methods:
      1. captureOutput(_:didOutput:from:): This delegate method is called whenever a new video frame is captured. It gets the pixel buffer from the frame and passes it to the recognizeText(in:) method to detect text.

      2. recognizeText(in:): This method uses Vision framework (VNRecognizeTextRequest) to perform text recognition on the captured video frame. The recognized text is checked to see if it contains a valid speed value (a number between 10 and 130, divisible by 10).

        • If a valid speed is detected, it updates the detectedSpeed binding to show the recognized speed.
      3. getCandidate(from:): This method processes multiple recognized text candidates and selects the most likely speed value based on:

        • High confidence (over 90%).
        • Speed range (10 to 130, divisible by 10).
        • Returning the most common speed value if multiple candidates are found.

    Conclusions

    This example dips our feet in the huge broad posibilites that will bring Vision Framework, not only limited to text detection but shapes also are possible.

    You can find source code used for writing this post in following repository. Also can play with this implementation in an app called Car Clip Camera placed at Apple Store.

    References

  • Seamless Text Input with Your Voice on iOS

    Seamless Text Input with Your Voice on iOS

    Most likely, you have faced a situation where you’re enjoying the seamless flow of an application—for instance, while making a train or hotel reservation. Then, suddenly—bam!—a never-ending form appears, disrupting the experience. I’m not saying that filling out such forms is irrelevant for the business—quite the opposite. However, as an app owner, you may notice in your analytics a significant drop in user conversions at this stage.

    In this post, I want to introduce a more seamless and user-friendly text input option to improve the experience of filling out multiple fields in a form.

    Base project

    To help you understand this topic better, we’ll start with a video presentation. Next, we’ll analyze the key parts of the code. You can also download the complete code from the repository linked below.

    To begin entering text, long-press the desired text field. When the bottom line turns orange, it indicates that the has been activated speech-to-text mode. Release your finger once you see the text correctly transcribed. If the transcribed text is correct, the line will turn green; otherwise, it will turn red.

    Let’s dig in the code…

    The view is built with a language picker, which is a crucial feature. It allows you to select the language you will use later, especially when interacting with a form containing multiple text fields.

    struct VoiceRecorderView: View {
       @StateObject private var localeManager = appSingletons.localeManager
        @State var name: String = ""
        @State var surename: String = ""
        @State var age: String = ""
        @State var email: String = ""
        var body: some View {
            Form {
                Section {
                    Picker("Select language", selection: $localeManager.localeIdentifier) {
                        ForEach(localeManager.locales, id: \.self) { Text($0).tag($0) }
                    }
                    .pickerStyle(SegmentedPickerStyle())
                    .onChange(of: localeManager.localeIdentifier) {
                    }
                }
    
                Section {
                    TextFieldView(textInputValue: $name,
                                  placeholder: "Name:",
                                  invalidFormatMessage: "Text must be greater than 6 characters!") { textInputValue in
                        textInputValue.count > 6
                    }
                    
                    TextFieldView(textInputValue: $surename,
                                  placeholder: "Surename:",
                                  invalidFormatMessage: "Text must be greater than 6 characters!") { textInputValue in
                        textInputValue.count > 6
                    }
                    TextFieldView(textInputValue: $age,
                                  placeholder: "Age:",
                                  invalidFormatMessage: "Age must be between 18 and 65") { textInputValue in
                        if let number = Int(textInputValue) {
                            return number >= 18 && number <= 65
                        }
                        return false
                    }
                }
                
                Section {
                    TextFieldView(textInputValue: $email,
                                  placeholder: "Email:",
                                  invalidFormatMessage: "Must be a valid email address") { textInputValue in
                        let emailRegex = #"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$"#
                        let emailPredicate = NSPredicate(format: "SELF MATCHES %@", emailRegex)
                        return emailPredicate.evaluate(with: textInputValue)
                    }
                }   
            }
            .padding()
        }
    }

    For every text field, we need a binding variable to hold the text field’s value, a placeholder for guidance, and an error message to display when the acceptance criteria function is not satisfied.

    When we examine the TextFieldView, we see that it is essentially a text field enhanced with additional features to improve user-friendliness.

    struct TextFieldView: View {
        
        @State private var isPressed = false
        
        @State private var borderColor = Color.gray
        @StateObject private var localeManager = appSingletons.localeManager
    
        @Binding var textInputValue: String
        let placeholder: String
        let invalidFormatMessage: String?
        var isValid: (String) -> Bool = { _ in true }
        
        var body: some View {
            VStack(alignment: .leading) {
                if !textInputValue.isEmpty {
                    Text(placeholder)
                        .font(.caption)
                }
                TextField(placeholder, text: $textInputValue)
                    .accessibleTextField(text: $textInputValue, isPressed: $isPressed)
                    .overlay(
                        Rectangle()
                            .frame(height: 2)
                            .foregroundColor(borderColor),
                        alignment: .bottom
                    )
                .onChange(of: textInputValue) { oldValue, newValue in
                        borderColor = getColor(text: newValue, isPressed: isPressed )
                }
                .onChange(of: isPressed) {
                        borderColor = getColor(text: textInputValue, isPressed: isPressed )
                }
                if !textInputValue.isEmpty,
                   !isValid(textInputValue),
                    let invalidFormatMessage {
                    Text(invalidFormatMessage)
                        .foregroundColor(Color.red)
                }
            }
        }
        
        func getColor(text: String, isPressed: Bool) -> Color {
            guard !isPressed else { return Color.orange }
            guard !text.isEmpty else { return Color.gray }
            return isValid(text) ? Color.green : Color.red
        }
        
    }

    The key point in the above code is the modifier .accessibleTextField, where all the magic of converting voice to text happens. We have encapsulated all speech-to-text functionality within this modifier.

    extension View {
        func accessibleTextField(text: Binding<String>, isPressed: Binding<Bool>) -> some View {
            self.modifier(AccessibleTextField(text: text, isPressed: isPressed))
        }
    }
    
    struct AccessibleTextField: ViewModifier {
        @StateObject private var viewModel = VoiceRecorderViewModel()
        
        @Binding var text: String
        @Binding var isPressed: Bool
        private let lock = NSLock()
        func body(content: Content) -> some View {
            content
                .onChange(of: viewModel.transcribedText) {
                    guard viewModel.transcribedText != "" else { return }
                    self.text = viewModel.transcribedText
                }
                .simultaneousGesture(
                    DragGesture(minimumDistance: 0)
                        .onChanged { _ in
                            lock.withLock {
                                if !isPressed {
                                    isPressed = true
                                    viewModel.startRecording(locale: appSingletons.localeManager.getCurrentLocale())
                                }
                            }
                            
                        }
                        .onEnded { _ in
                            
                            if isPressed {
                                lock.withLock {
                                    isPressed = false
                                    viewModel.stopRecording()
                                }
                            }
                        }
                )
        }
    }

    The voice-to-text functionality is implemented in the VoiceRecorderViewModel. In the view, it is controlled by detecting a long press from the user to start recording and releasing to stop the recording. The transcribed voice text is then forwarded upward via the text Binding attribute.

    Finally, here is the view model that handles the transcription:

    import Foundation
    import AVFoundation
    import Speech
    
    class VoiceRecorderViewModel: ObservableObject {
        @Published var transcribedText: String = ""
        @Published var isRecording: Bool = false
        
        private var audioRecorder: AVAudioRecorder?
        private let audioSession = AVAudioSession.sharedInstance()
        private let recognitionRequest = SFSpeechAudioBufferRecognitionRequest()
        private var recognitionTask: SFSpeechRecognitionTask?
        private var audioEngine = AVAudioEngine()
        
        var speechRecognizer: SFSpeechRecognizer?
    
        func startRecording(locale: Locale) {
            do {
                self.speechRecognizer = SFSpeechRecognizer(locale: locale)
    
                recognitionTask?.cancel()
                recognitionTask = nil
    
                try audioSession.setCategory(.record, mode: .measurement, options: .duckOthers)
                try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
    
                guard let recognizer = speechRecognizer, recognizer.isAvailable else {
                    transcribedText = "Reconocimiento de voz no disponible para el idioma seleccionado."
                    return
                }
                
                let inputNode = audioEngine.inputNode
                let recordingFormat = inputNode.outputFormat(forBus: 0)
                inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { buffer, when in
                    self.recognitionRequest.append(buffer)
                }
                
                audioEngine.prepare()
                try audioEngine.start()
                
                recognitionTask = recognizer.recognitionTask(with: recognitionRequest) { result, error in
                    if let result = result {
                        self.transcribedText = result.bestTranscription.formattedString
                    }
                }
                
                isRecording = true
            } catch {
                transcribedText = "Error al iniciar la grabación: \(error.localizedDescription)"
            }
        }
        
        func stopRecording() {
            audioEngine.stop()
            audioEngine.inputNode.removeTap(onBus: 0)
            recognitionRequest.endAudio()
            recognitionTask?.cancel()
            isRecording = false
        }
    }

    Key Components

    1. Properties:

      • @Published var transcribedText: Holds the real-time transcribed text, allowing SwiftUI views to bind and update dynamically.
      • @Published var isRecording: Indicates whether the application is currently recording.
      • audioRecorder, audioSession, recognitionRequest, recognitionTask, audioEngine, speechRecognizer: These manage audio recording and speech recognition.
    2. Speech Recognition Workflow:

      • SFSpeechRecognizer: Recognizes and transcribes speech from audio input for a specified locale.
      • SFSpeechAudioBufferRecognitionRequest: Provides an audio buffer for speech recognition tasks.
      • AVAudioEngine: Captures microphone input.

    Conclusions

    I aim you that you download the project  from following github repositoryand start to play with such great techology.

    References

    • Speech

      Apple Developer Documentation

  • Writing a Barcode Reader App in No Time

    Writing a Barcode Reader App in No Time

    There are other ways to input data into your application besides using a device’s keyboard. One such method is reading barcodes. In this post, I’ll demonstrate how easy it is to implement a solution for this functionality.

    AVCaptureMetadataOutput

    AVCaptureMetadataOutput is the class responsible for intercepting metadata objects from the video stream captured during a session. Part of the AVFoundation framework, its primary purpose is to detect and process metadata in real-time while capturing video.

    Key Characteristics of AVCaptureMetadataOutput:
    1. Code Detection:
      This class can detect various types of codes, such as QR codes and barcodes, including formats like EAN-8, EAN-13, UPC-E, Code39, and Code128, among others.

    2. Flexible Configuration:
      You can specify the types of metadata you want to capture using the metadataObjectTypes property. This provides granular control over the kind of information the system processes.

    3. Delegate-Based Processing:
      Metadata detection and processing are managed via a delegate object. This approach provides flexibility in handling the detected data and enables custom responses. However, note that working with this delegate often requires integration with the UIKit framework for user interface handling.

    4. Integration with AVCaptureSession:
      The AVCaptureMetadataOutput instance is added as an output to an AVCaptureSession. This setup enables real-time processing of video data as it is captured.

    Creating iOS App sample app

    Create a new blank iOS SwiftUI APP, and do not forget set Strict Concurrency Checking to Complete and Swift Language Versionto Swift 6

    As I mention on point 3 from past section, the pattern that implements AVCaptureMetadataOutput is deletage patterns, but we want our app that uses the latest and coolest SwiftUI framework. For fixing that we will need support of our old friend UIKit. Basically wrap UIKit ViewController into a UIViewControllerRespresentable, for being accessible from SwiftUI. And finally implement delegate inside UIViewControllerRespresentable.

    Create a new file called ScannerPreview and start writing following code:

    import SwiftUI
    import AVFoundation
    
    // 1
    struct ScannerPreview: UIViewControllerRepresentable {
        @Binding var isScanning: Bool
        var didFindBarcode: (String) -> Void = { _ in }
        // 2
        func makeCoordinator() -> Coordinator {
            return Coordinator(parent: self)
        }
        // 3
        func makeUIViewController(context: Context) -> UIViewController {
            let viewController = UIViewController()
            let captureSession = AVCaptureSession()
    
            // Setup the camera input
            guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else { return viewController }
            let videoDeviceInput: AVCaptureDeviceInput
    
            do {
                videoDeviceInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
            } catch {
                return viewController
            }
    
            if (captureSession.canAddInput(videoDeviceInput)) {
                captureSession.addInput(videoDeviceInput)
            } else {
                return viewController
            }
    
            // Setup the metadata output
            let metadataOutput = AVCaptureMetadataOutput()
    
            if (captureSession.canAddOutput(metadataOutput)) {
                captureSession.addOutput(metadataOutput)
    
                metadataOutput.setMetadataObjectsDelegate(context.coordinator, queue: DispatchQueue.main)
                metadataOutput.metadataObjectTypes = [.ean13, .ean8, .pdf417, .upce, .qr, .aztec] // Add other types if needed
            } else {
                return viewController
            }
    
            // Setup preview layer
            let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
            previewLayer.frame = viewController.view.layer.bounds
            previewLayer.videoGravity = .resizeAspectFill
            viewController.view.layer.addSublayer(previewLayer)
    
            captureSession.startRunning()
    
            return viewController
        }
    
        func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
            // Here we can update the UI if needed (for example, stopping the session)
        }
    }

    To integrate a UIViewController into a SwiftUI View, import SwiftUI (for access to UIViewControllerRepresentable) and AVFoundation (for AVCaptureMetadataOutputObjectsDelegate).

    Key Features and Implementation
      1. UIViewControllerRepresentable Protocol
        Implementing the UIViewControllerRepresentable protocol allows a UIKit UIViewController to be reused within SwiftUI.

        • isScanning: This is a binding to the parent view, controlling the scanning state.
        • didFindBarcode: A callback function that is executed whenever a barcode is successfully scanned and read.
      2. Coordinator and Bridging

        • makeCoordinator: This method is required to fulfill the UIViewControllerRepresentable protocol. It creates a «bridge» (e.g., a broker, intermediary, or proxy) between the UIKit UIViewController and the SwiftUI environment. In this implementation, the Coordinator class conforms to the AVCaptureMetadataOutputObjectsDelegate protocol, which handles metadata detection and processing.
      3. Creating the UIViewController

        • makeUIViewController: Another required method in the protocol, responsible for returning a configured UIViewController.
          • Inside this method, the AVCaptureSession is set up to detect specific barcode formats (e.g., EAN-13, EAN-8, PDF417, etc.).
          • The configured session is added as a layer to the UIViewController.view.
        func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
            // Here we can update the UI if needed (for example, stopping the session)
        }
        
        //1
        @MainActor
        class Coordinator: NSObject, @preconcurrency AVCaptureMetadataOutputObjectsDelegate {
            var parent: ScannerPreview
            
            init(parent: ScannerPreview) {
                self.parent = parent
            }
            // 2
            // MARK :- AVCaptureMetadataOutputObjectsDelegate
            func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
                // 4
                if let metadataObject = metadataObjects.first {
                    guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else { return }
                    guard let stringValue = readableObject.stringValue else { return }
                    AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
                    self.parent.isScanning = false
                    // 3
                    parent.didFindBarcode(String(stringValue))
                }
            }
        }

    Later, we will implement the Coordinator class, which must inherit from NSObject because it needs to conform to the AVCaptureMetadataOutputObjectsDelegate protocol, an extension of NSObjectProtocol.

    Key Features and Implementation:
    1. Swift 6 Compliance and Data Race Avoidance
      To ensure compliance with Swift 6 and avoid data races, the class is executed on @MainActor. This is necessary because it interacts with attributes from its parent, UIViewControllerRepresentable. Since AVCaptureMetadataOutput operates in a non-isolated domain, we’ve marked the class with @MainActor.

    2. Thread Safety
      Before marking AVCaptureMetadataOutputObjectsDelegate with @preconcurrency, ensure the following:

      • The metadataOutput.setMetadataObjectsDelegate(context.coordinator, queue: DispatchQueue.main) call is executed on the main thread (@MainActor).
      • This guarantees that when setting up AVCaptureMetadataOutput, it operates safely on the main thread.
    3. Data Handling
      The parent view receives a copy of the scanned barcode string. At no point does the delegate implementation modify the received data. This ensures thread safety and avoids potential data races.

    4. Protocol Method Implementation
      In the protocol method implementation:

      • Fetch the first object.
      • Retrieve the barcode value.
      • Update the scanning state.
      • Execute the callback function.

    By ensuring that no data is modified across different isolated domains, it is safe to proceed with marking the protocol with @preconcurrency.

     

    Final step is just implent the SwiftUI view where ScannerPreview view will be embeded. Create a new file called BarcodeScannerView and write following code:

    import SwiftUI
    import AVFoundation
    
    struct BarcodeScannerView: View {
        @State private var scannedCode: String?
        @State private var isScanning = true
        @State private var showAlert = false
        
        var body: some View {
            VStack {
                Text("Scan a Barcode")
                    .font(.largeTitle)
                    .padding()
    
                ZStack {
                    //1
                    ScannerPreview(isScanning: $isScanning,
                                   didFindBarcode: { value in
                        scannedCode = value
                        showAlert = true
                    }).edgesIgnoringSafeArea(.all)
    
                    VStack {
                        Spacer()
                        HStack {
                            Spacer()
                            if let scannedCode = scannedCode {
                                Text("Scanned Code: \(scannedCode)")
                                    .font(.title)
                                    .foregroundColor(.white)
                                    .padding()
                            }
                            Spacer()
                        }
                        Spacer()
                    }
                }
    
                if !isScanning {
                    Button("Start Scanning Again") {
                        self.isScanning = true
                        self.scannedCode = nil
                    }
                    .padding()
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(8)
                }
            }
            .onAppear {
                self.scannedCode = nil
                self.isScanning = true
            }
        }
    }
    Key Features and Implementation:
    1. Just place the preview in a ZStack and implment the callback to execute when the barcode is read.

    import SwiftUI
    
    struct ContentView: View {
        var body: some View {
            BarcodeScannerView()
        }
    }
    
    #Preview {
        ContentView()
    }
    

    Last but not least be sure that ContentView is executing the view that we have just created. And be sure that you have a description for NSCameraUsageDescription setting.

    Build and Run on real device

    For executing the app be sure that you deploy on a real device (iPhone or iPad). Whem the app ask you permission for using the camera, obviously say allow.

    Conclusions

    In this post, you have seen how easy it is to implement a barcode scanner using native libraries. You can find the working code used in this post in the following repository.

    References