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!
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.