I have been trying to copy some of the features of the Apple Notes App,i.e., the scanning feature. I have managed to implement almost everything. However, I can't just overcome this Error: Cannot find 'VNDetectDocumentRectanglesRequest' in scope. I have already added the Vision, VisionKit and CoreImage frameworks. I am targeting iOS 14 or newer.
import SwiftUI
import AVFoundation
import Vision
import VisionKit
struct TakingPhotoScreen: View {
@Binding var isPhotoTaken: Bool
@StateObject private var cameraViewModel = CameraViewModel()
@State private var isProcessing = false
@State private var showError = false
@State private var showFlash = false
@State private var showAutoCaptureGuide = true
@State private var showManualAlignmentGuide = false
@State private var detectedRectangle: CGRect?
@State private var isDocumentAligned = false
var body: some View {
ZStack {
Color.black.edgesIgnoringSafeArea(.all)
VStack {
HStack {
Spacer()
Text("Scan the Ballot")
.font(.custom("Helvetica Neue", size: 24))
.foregroundColor(.white)
.padding(.trailing)
}
// Camera preview layer
CameraPreviewLayer(session: cameraViewModel.captureSession)
.overlay(
GeometryReader { geometry in
if let rectangle = detectedRectangle, !isDocumentAligned {
DocumentAlignmentOverlay(rectangle: rectangle, imageSize: geometry.size)
.stroke(Color.green, lineWidth: 2)
}
}
)
.overlay(
VStack {
if showAutoCaptureGuide {
AutoCaptureGuide()
} else if showManualAlignmentGuide {
ManualAlignmentGuide(isDocumentAligned: $isDocumentAligned)
}
}
)
Button(action: {
capturePhoto()
}) {
Text("Capture Photo")
.font(.custom("Helvetica Neue Bold", size: 18))
.foregroundColor(.white)
.padding()
.background(Color.blue)
.cornerRadius(10)
.disabled(isProcessing)
}
.padding()
Button(action: {
toggleFlash()
}) {
Image(systemName: showFlash ? "bolt.fill" : "bolt.slash.fill")
.font(.system(size: 24))
.foregroundColor(showFlash ? .yellow : .white)
}
.padding(.bottom, 20)
if showError {
Text("Error capturing photo. Please try again.")
.font(.custom("Helvetica Neue", size: 16))
.foregroundColor(.red)
.padding(.bottom, 20)
}
}
}
.edgesIgnoringSafeArea(.all)
.onAppear {
cameraViewModel.startSession()
}
.onDisappear {
cameraViewModel.stopSession()
}
}
func capturePhoto() {
isProcessing = true
showError = false
cameraViewModel.capturePhoto { image in
if let image = image {
processImage(image)
} else {
showError = true
isProcessing = false
}
}
}
func processImage(_ image: UIImage?) {
guard let image = image else {
showError = true
isProcessing = false
return
}
// Convert UIImage to CIImage
guard let ciImage = CIImage(image: image) else {
showError = true
isProcessing = false
return
}
// Perform document scanning using Vision and Core Image
let handler = VNImageRequestHandler(ciImage: ciImage, orientation: .up)
do {
let documentScanRequest = VNDetectDocumentRectanglesRequest { request, error in
if let error = error {
print("Error detecting document rectangle: \(error)")
self.showError = true
self.isProcessing = false
return
}
guard let observations = request.results as? [VNRectangleObservation],
let detectedRectangle = observations.first?.boundingBox else {
print("No document rectangle detected.")
self.showError = true
self.isProcessing = false
return
}
self.detectedRectangle = detectedRectangle
if self.isDocumentAligned {
// Crop the document from the image using the detected rectangle
let croppedImage = ciImage.cropped(to: detectedRectangle)
// Perform further processing or analysis with the cropped image
// Set the processed result or pass it to the next screen
DispatchQueue.main.async {
// Example code to pass the processed image to the next screen
// Replace with your own logic or data flow
let processedImage = UIImage(ciImage: croppedImage)
cameraViewModel.processedPhoto = processedImage
isPhotoTaken = true
isProcessing = false
}
} else {
showManualAlignmentGuide = true
showAutoCaptureGuide = false
isProcessing = false
}
}
try handler.perform([documentScanRequest])
} catch {
print("Error processing document scan request: \(error)")
showError = true
isProcessing = false
}
}
func toggleFlash() {
cameraViewModel.toggleFlash { success in
if success {
showFlash.toggle()
}
}
}
}
struct CameraPreviewLayer: UIViewRepresentable {
var session: AVCaptureSession
func makeUIView(context: Context) -> UIView {
let view = UIView()
let layer = AVCaptureVideoPreviewLayer(session: session)
layer.videoGravity = .resizeAspectFill
layer.frame = view.bounds
view.layer.addSublayer(layer)
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
uiView.frame = UIScreen.main.bounds
}
}
struct DocumentAlignmentOverlay: Shape {
var rectangle: CGRect
var imageSize: CGSize
func path(in rect: CGRect) -> Path {
var path = Path()
let scaleX = rect.width / imageSize.width
let scaleY = rect.height / imageSize.height
let transformedRect = rectangle.applying(CGAffineTransform(scaleX: scaleX, y: scaleY))
path.addRect(transformedRect)
return path
}
}
struct ManualAlignmentGuide: View {
@Binding var isDocumentAligned: Bool
var body: some View {
VStack {
Image(systemName: "hand.draw.fill")
.font(.system(size: 30))
.foregroundColor(.white)
.padding(.bottom, 10)
Text("Adjust the alignment for accurate scanning.")
.font(.custom("Helvetica Neue", size: 16))
.foregroundColor(.white)
.padding(.bottom, 20)
Button(action: {
isDocumentAligned = true
}) {
Text("Continue")
.font(.custom("Helvetica Neue Bold", size: 18))
.foregroundColor(.white)
.padding()
.background(Color.blue)
.cornerRadius(10)
}
}
}
}
class CameraViewModel: NSObject, ObservableObject, AVCapturePhotoCaptureDelegate {
public let captureSession = AVCaptureSession()
private let photoOutput = AVCapturePhotoOutput()
@Published var processedPhoto: UIImage?
@Published var isFlashEnabled = false
override init() {
super.init()
setupCaptureSession()
}
func startSession() {
captureSession.startRunning()
}
func stopSession() {
captureSession.stopRunning()
}
func capturePhoto(completion: @escaping (UIImage?) -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
let settings = AVCapturePhotoSettings()
if self.isFlashEnabled {
settings.flashMode = .on
}
self.photoOutput.capturePhoto(with: settings, delegate: self)
DispatchQueue.main.async {
completion(self.processedPhoto)
}
}
}
func toggleFlash(completion: @escaping (Bool) -> Void) {
guard let device = AVCaptureDevice.default(for: .video),
device.hasTorch else {
completion(false)
return
}
do {
try device.lockForConfiguration()
if device.torchMode == .on {
device.torchMode = .off
isFlashEnabled = false
} else {
try device.setTorchModeOn(level: AVCaptureDevice.maxAvailableTorchLevel)
isFlashEnabled = true
}
device.unlockForConfiguration()
completion(true)
} catch {
print("Error toggling flash: \(error)")
completion(false)
}
}
private func setupCaptureSession() {
guard let device = AVCaptureDevice.default(for: .video),
let input = try? AVCaptureDeviceInput(device: device) else {
return
}
if captureSession.canAddInput(input) {
captureSession.addInput(input)
}
if captureSession.canAddOutput(photoOutput) {
captureSession.addOutput(photoOutput)
}
}
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
guard let imageData = photo.fileDataRepresentation(),
let image = UIImage(data: imageData) else {
processedPhoto = nil
return
}
processedPhoto = image
}
}
struct AutoCaptureGuide: View {
var body: some View {
VStack {
Image(systemName: "camera.metering.center.weighted")
.font(.system(size: 40))
.foregroundColor(.white)
.padding(.bottom, 10)
Text("Position the document within the frame.")
.font(.custom("Helvetica Neue", size: 16))
.foregroundColor(.white)
.padding(.bottom, 20)
Image(systemName: "hand.point.up.fill")
.font(.system(size: 30))
.foregroundColor(.white)
}
}
}
struct ContentView: View {
@State private var isPhotoTaken = false
var body: some View {
if isPhotoTaken {
// Add code for the confirmation screen or other views
Text("Confirmation Screen")
} else {
TakingPhotoScreen(isPhotoTaken: $isPhotoTaken)
}
}
}