import SwiftUI struct CameraScreen: View { @State private var grabber = CameraFrameGrabber() @State private var status = "In attesa permesso camera..." @State private var lastShot: UIImage? var body: some View { ZStack { CameraPreviewView(session: grabber.captureSession) .ignoresSafeArea() VStack { HStack { Text(status) .font(.footnote) .padding(8) .background(.ultraThinMaterial) .cornerRadius(10) Spacer() } .padding() Spacer() HStack(spacing: 12) { Button("Scatta frame") { lastShot = grabber.snapshotUIImage() } .buttonStyle(.borderedProminent) if let img = lastShot { Image(uiImage: img) .resizable() .scaledToFit() .frame(width: 90, height: 120) .cornerRadius(10) .background(.ultraThinMaterial) } } .padding() } } .task { let ok = await CameraFrameGrabber.requestCameraPermission() guard ok else { status = "Permesso camera negato (Settings → Privacy → Camera)" return } status = "Avvio sessione..." grabber.start { error in Task { @MainActor in status = "Errore: \(error.localizedDescription)" } } status = "Camera attiva" } .onDisappear { grabber.stop() } } } import SwiftUI import AVFoundation import UIKit struct CameraPreviewView: UIViewRepresentable { let session: AVCaptureSession func makeUIView(context: Context) -> PreviewUIView { let v = PreviewUIView() v.videoPreviewLayer.session = session v.videoPreviewLayer.videoGravity = .resizeAspectFill return v } func updateUIView(_ uiView: PreviewUIView, context: Context) { uiView.videoPreviewLayer.session = session } } final class PreviewUIView: UIView { override class var layerClass: AnyClass { AVCaptureVideoPreviewLayer.self } var videoPreviewLayer: AVCaptureVideoPreviewLayer { layer as! AVCaptureVideoPreviewLayer } } import AVFoundation import UIKit import CoreImage import os final class CameraFrameGrabber: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate { private let log = Logger(subsystem: "test_camera_view", category: "Camera") private let session = AVCaptureSession() private let sessionQueue = DispatchQueue(label: "camera.session.queue") private let outputQueue = DispatchQueue(label: "camera.output.queue") private let ciContext = CIContext() private var latestPixelBuffer: CVPixelBuffer? private let bufferLock = NSLock() var captureSession: AVCaptureSession { session } // MARK: - Permission static func requestCameraPermission() async -> Bool { switch AVCaptureDevice.authorizationStatus(for: .video) { case .authorized: return true case .notDetermined: return await AVCaptureDevice.requestAccess(for: .video) case .denied, .restricted: return false @unknown default: return false } } // MARK: - Start/Stop func start(onError: @escaping (Error) -> Void) { sessionQueue.async { [weak self] in guard let self else { return } do { try self.configureSessionIfNeeded() self.installObservers() self.session.startRunning() self.log.info("Capture session started.") } catch { self.log.error("Failed to start session: \(error.localizedDescription, privacy: .public)") onError(error) } } } func stop() { sessionQueue.async { [weak self] in guard let self else { return } if self.session.isRunning { self.session.stopRunning() } self.removeObservers() self.bufferLock.lock() self.latestPixelBuffer = nil self.bufferLock.unlock() self.log.info("Capture session stopped.") } } // MARK: - Snapshot func snapshotUIImage() -> UIImage? { bufferLock.lock() guard let pb = latestPixelBuffer else { bufferLock.unlock(); return nil } bufferLock.unlock() let ciImage = CIImage(cvPixelBuffer: pb) guard let cgImage = ciContext.createCGImage(ciImage, from: ciImage.extent) else { return nil } return UIImage(cgImage: cgImage) } // MARK: - Delegate func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { guard let pb = CMSampleBufferGetImageBuffer(sampleBuffer) else { return } bufferLock.lock() latestPixelBuffer = pb bufferLock.unlock() } // MARK: - Configuration private var configured = false private func configureSessionIfNeeded() throws { guard !configured else { return } session.beginConfiguration() session.sessionPreset = .hd1280x720 guard let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else { throw NSError(domain: "CameraFrameGrabber", code: 1, userInfo: [NSLocalizedDescriptionKey: "Camera non disponibile"]) } let input = try AVCaptureDeviceInput(device: device) guard session.canAddInput(input) else { throw NSError(domain: "CameraFrameGrabber", code: 2, userInfo: [NSLocalizedDescriptionKey: "Impossibile aggiungere input camera"]) } session.addInput(input) let videoOutput = AVCaptureVideoDataOutput() videoOutput.alwaysDiscardsLateVideoFrames = true videoOutput.videoSettings = [ kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange ] videoOutput.setSampleBufferDelegate(self, queue: outputQueue) guard session.canAddOutput(videoOutput) else { throw NSError(domain: "CameraFrameGrabber", code: 3, userInfo: [NSLocalizedDescriptionKey: "Impossibile aggiungere output video"]) } session.addOutput(videoOutput) if let conn = videoOutput.connection(with: .video), conn.isVideoOrientationSupported { conn.videoOrientation = .portrait } session.commitConfiguration() configured = true } // MARK: - Observers (debug errori) private var observersInstalled = false private func installObservers() { guard !observersInstalled else { return } observersInstalled = true NotificationCenter.default.addObserver(self, selector: #selector(runtimeError(_:)), name: .AVCaptureSessionRuntimeError, object: session) NotificationCenter.default.addObserver(self, selector: #selector(wasInterrupted(_:)), name: .AVCaptureSessionWasInterrupted, object: session) NotificationCenter.default.addObserver(self, selector: #selector(interruptionEnded(_:)), name: .AVCaptureSessionInterruptionEnded, object: session) } private func removeObservers() { guard observersInstalled else { return } observersInstalled = false NotificationCenter.default.removeObserver(self, name: .AVCaptureSessionRuntimeError, object: session) NotificationCenter.default.removeObserver(self, name: .AVCaptureSessionWasInterrupted, object: session) NotificationCenter.default.removeObserver(self, name: .AVCaptureSessionInterruptionEnded, object: session) } @objc private func runtimeError(_ n: Notification) { let err = (n.userInfo?[AVCaptureSessionErrorKey] as? NSError) log.error("Runtime error: \(err?.localizedDescription ?? "unknown", privacy: .public)") } @objc private func wasInterrupted(_ n: Notification) { let reason = (n.userInfo?[AVCaptureSessionInterruptionReasonKey] as? NSNumber)?.intValue ?? -1 log.warning("Session interrupted. reason=\(reason, privacy: .public)") } @objc private func interruptionEnded(_ n: Notification) { log.info("Interruption ended.") } }