Restart Camera when view appears

236 Views Asked by At

I'm having a problem with my app. I'm trying to build a barcode reader inside a view. When I start my view for the first time the reader works fine, but if I switch to another view and then go back to the previous one (where the barcode is), my camera stops working. I want to make it working everytime i switch back to the view where the barcode is.

Here's my code:

import AVKit
import Foundation
import SwiftUI
import VisionKit

enum ScanType: String {
    case barcode, text
}

enum DataScannerAccessStatusType {
    case notDetermined
    case cameraAccessNotGranted
    case cameraNotAvailable
    case scannerAvailable
    case scannerNotAvailable
}

@MainActor
final class ScannerDataViewModel: ObservableObject {
    
    @State private var session: AVCaptureSession = .init()
    @Published var dataScannerAccessStatus: DataScannerAccessStatusType = .notDetermined
    @Published var recognizedItems: [RecognizedItem] = []
    @Published var scanType: ScanType = .barcode
    @Published var textContentType: DataScannerViewController.TextContentType?
    @Published var recognizesMultipleItems = true
    
    var recognizedDataType: DataScannerViewController.RecognizedDataType {
        scanType == .barcode ? .barcode() : .text(textContentType: textContentType)
    }
    
    var headerText: String {
        if recognizedItems.isEmpty {
            return "Scansione \(scanType.rawValue)"
        } else {
            return "Riconosciuti \(recognizedItems.count) oggetti"
        }
    }
    
      var dataScannerViewId: Int {
        var hasher = Hasher()
        hasher.combine(scanType)
        hasher.combine(recognizesMultipleItems)
        if let textContentType {
            hasher.combine(textContentType)
        }
        return hasher.finalize()
    }
    
    private var isScannerAvailable: Bool {
        DataScannerViewController.isAvailable && DataScannerViewController.isSupported
    }
    
    func requestDataScannerAccessStatus() async {
        guard UIImagePickerController.isSourceTypeAvailable(.camera) else {
            dataScannerAccessStatus = .cameraNotAvailable
            return
        }
        
        switch AVCaptureDevice.authorizationStatus(for: .video) {
            
        case .authorized:
            dataScannerAccessStatus = isScannerAvailable ? .scannerAvailable : .scannerNotAvailable
            
        case .restricted, .denied:
            dataScannerAccessStatus = .cameraAccessNotGranted
            
        case .notDetermined:
            let granted = await AVCaptureDevice.requestAccess(for: .video)
            if granted {
                dataScannerAccessStatus = isScannerAvailable ? .scannerAvailable : .scannerNotAvailable
            } else {
                dataScannerAccessStatus = .cameraAccessNotGranted
            }
        
        default: break
            
        }
    }
}

This is DataScannerView:

import Foundation
import SwiftUI
import VisionKit

struct DataScannerView: UIViewControllerRepresentable {
    
    @Binding var recognizedItems: [RecognizedItem]
    let recognizedDataType: DataScannerViewController.RecognizedDataType
    let recognizesMultipleItems: Bool
    
    
    func makeUIViewController(context: Context) -> DataScannerViewController {
        let vc = DataScannerViewController(
            recognizedDataTypes: [recognizedDataType],
            qualityLevel: .balanced,
            recognizesMultipleItems: recognizesMultipleItems,
            isGuidanceEnabled: true,
            isHighlightingEnabled: true
        )
        return vc
    }
    
    func updateUIViewController(_ uiViewController: DataScannerViewController, context: Context) {
        uiViewController.delegate = context.coordinator
        try? uiViewController.startScanning()
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(recognizedItems: $recognizedItems)
    }
    
    static func dismantleUIViewController(_ uiViewController: DataScannerViewController, coordinator: Coordinator) {
        uiViewController.stopScanning()
    }
    
    
    class Coordinator: NSObject, DataScannerViewControllerDelegate {
        
        @Binding var recognizedItems: [RecognizedItem]

        init(recognizedItems: Binding<[RecognizedItem]>) {
            self._recognizedItems = recognizedItems
        }
        
        func dataScanner(_ dataScanner: DataScannerViewController, didTapOn item: RecognizedItem) {
            print("didTapOn \(item)")
        }
        
        func dataScanner(_ dataScanner: DataScannerViewController, didAdd addedItems: [RecognizedItem], allItems: [RecognizedItem]) {
            UINotificationFeedbackGenerator().notificationOccurred(.success)
            recognizedItems.append(contentsOf: addedItems)
            print("didAddItems \(addedItems)")
        }
        
        func dataScanner(_ dataScanner: DataScannerViewController, didRemove removedItems: [RecognizedItem], allItems: [RecognizedItem]) {
            self.recognizedItems = recognizedItems.filter { item in
                !removedItems.contains(where: {$0.id == item.id })
            }
            print("didRemovedItems \(removedItems)")
        }
        
        func dataScanner(_ dataScanner: DataScannerViewController, becameUnavailableWithError error: DataScannerViewController.ScanningUnavailable) {
            print("became unavailable with error \(error.localizedDescription)")
        }
        
    }
    
}

and finally this is the view where I want to show my barcode reader:

import SwiftUI

struct ScanView: View {
    
    @Environment(\.presentationMode) var presentationMode
    @StateObject private var vm: ScannerDataViewModel = ScannerDataViewModel()
    private let viewInsideScan = ViewInsideScanView()
    
    var body: some View {
        NavigationView {
            ZStack {
                Color("whiteBackground").ignoresSafeArea()
                VStack(spacing: 8) {
                    
                    Text("Posiziona il codice a barre al centro")
                        .font(.title3)
                        .foregroundColor(Color.primary)
                        .padding(.top, 20)
                    
                    Text("La scansione inizierà automaticamente")
                        .font(.callout)
                        .foregroundColor(Color.primary)
                    
                    Spacer()
                    
                    CenteredSquareView(content: viewInsideScan)
                        .frame(width: 350, height: 350, alignment: .center)
                    
                    Spacer()
                    
                    Image(systemName: "qrcode.viewfinder")
                        .font(.largeTitle)
                        .foregroundColor(.gray)
                    
                    
                    Text("Tocca l'icona per eseguire una nuova scansione")
                        .font(.callout)
                        .foregroundColor(Color.primary)
                    
                    Spacer(minLength: 45)
                    
                }
            }
            .environmentObject(vm)
            .onAppear {
                Task {
                    await vm.requestDataScannerAccessStatus()
                }
            }
        }
    }
}

struct CenteredSquareView<Content: View>: View {
    var content: Content
    
    var body: some View {
        GeometryReader { geometry in
            ZStack {
                content
                    .frame(width: geometry.size.width * 1, height: geometry.size.width * 1)
                    .cornerRadius(20)
                    .position(x: geometry.size.width / 2, y: geometry.size.height / 2) // Posiziona al centro
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

#Preview {
    ScanView()
}

I know, this is super confusing. I'm a Junior Dev and I'm trying to getting better. I noticed that everytime i switch to the view where the reader is, my camera works for few seconds and then it stops. Thanks everyone for your help!

2

There are 2 best solutions below

1
On

It looks like the issue might be related to how the DataScannerView is being used in your ScanView. Specifically, when you switch to another view and then come back, the DataScannerView is being re-created, and it might not be handling the transition back to the view properly.

Try modifying your DataScannerView to handle the start and stop of the scanning session more explicitly. You can do this by using the onAppear and onDisappear modifiers to start and stop the scanning session when the view appears and disappears.

Share you project in GitHub to check.

0
On

DataScannerViewController is more or less static after it has been created. This means you cannot update recognizedDataTypes and recognizesMultipleItems "on the fly" without recreating it completely. To solve this, you have created the dataScannerViewId in ScannerDataViewModel and you use it with the id() SwiftUI modifier.

Now you face the issue that (when ScanView is embedded in a TabView!) scanning does not continue automatically when the view re-appears, even if you call startScanning() in DataScannerView.updateUIViewController: the camera image stays frozen and no scanning works.

To solve this you can force recreation of the DataScannerView instance using your dataScannerViewId by adding a @Published var appearCount = 0 to ScannerDataViewModel, including this value in dataScannerViewId calculation and incrementing it whenever the mainView appears on screen (using .onAppear { vm.appearCount += 1 } modifier). I successfully verified this solution (see GitHub DataScannerTest project), but I would rather call it a workaround. An alternative solution would be the presentation of the DataScannerView in a sheet.
Better would be to file a feedback to Apple to get it properly fixed in a future version.

I suppose the problem is caused by DataScannerViewController overriding the viewWillAppear() and viewDidDisappear() in some special way which does not properly work in combination with SwiftUIs TabView.