diff --git a/Memola.xcodeproj/project.pbxproj b/Memola.xcodeproj/project.pbxproj index f0f51e5..861bf2f 100644 --- a/Memola.xcodeproj/project.pbxproj +++ b/Memola.xcodeproj/project.pbxproj @@ -84,6 +84,7 @@ ECA738FC2BE61C5200A4542E /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738FB2BE61C5200A4542E /* Persistence.swift */; }; ECA739082BE623F300A4542E /* PenDock.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA739072BE623F300A4542E /* PenDock.swift */; }; ECBE52962C1D5900006BDB3D /* PhotoPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECBE52952C1D5900006BDB3D /* PhotoPreview.swift */; }; + ECBE52992C1D60E5006BDB3D /* CameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECBE52982C1D60E5006BDB3D /* CameraView.swift */; }; ECD12A862C19EE3900B96E12 /* ElementObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A852C19EE3900B96E12 /* ElementObject.swift */; }; ECD12A8A2C19EFB000B96E12 /* Element.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A892C19EFB000B96E12 /* Element.swift */; }; ECD12A8C2C1AEAA900B96E12 /* PhotoObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A8B2C1AEAA900B96E12 /* PhotoObject.swift */; }; @@ -183,6 +184,7 @@ ECA738FB2BE61C5200A4542E /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; ECA739072BE623F300A4542E /* PenDock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenDock.swift; sourceTree = ""; }; ECBE52952C1D5900006BDB3D /* PhotoPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPreview.swift; sourceTree = ""; }; + ECBE52982C1D60E5006BDB3D /* CameraView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraView.swift; sourceTree = ""; }; ECD12A852C19EE3900B96E12 /* ElementObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementObject.swift; sourceTree = ""; }; ECD12A892C19EFB000B96E12 /* Element.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Element.swift; sourceTree = ""; }; ECD12A8B2C1AEAA900B96E12 /* PhotoObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoObject.swift; sourceTree = ""; }; @@ -232,6 +234,7 @@ EC1B783A2BF9C68C005A34E2 /* Views */ = { isa = PBXGroup; children = ( + ECBE52972C1D6087006BDB3D /* CameraView */, ECFC51252BF8885000D0D051 /* ColorPicker */, ); path = Views; @@ -676,6 +679,14 @@ path = PhotoPreview; sourceTree = ""; }; + ECBE52972C1D6087006BDB3D /* CameraView */ = { + isa = PBXGroup; + children = ( + ECBE52982C1D60E5006BDB3D /* CameraView.swift */, + ); + path = CameraView; + sourceTree = ""; + }; ECD12A872C19EF8700B96E12 /* Elements */ = { isa = PBXGroup; children = ( @@ -890,6 +901,7 @@ EC0D14212BF79C73009BFE5F /* ToolObject.swift in Sources */, EC50500D2BF6674400B4D86E /* OnDragViewModifier.swift in Sources */, EC9AB09F2C1401A40076AF58 /* EraserObject.swift in Sources */, + ECBE52992C1D60E5006BDB3D /* CameraView.swift in Sources */, ECA7389E2BE601CB00A4542E /* QuadVertex.swift in Sources */, ECA738B32BE60D9E00A4542E /* CanvasView.swift in Sources */, ECA738C42BE60E8800A4542E /* MarkerPenStyle.swift in Sources */, diff --git a/Memola/Components/Views/CameraView/CameraView.swift b/Memola/Components/Views/CameraView/CameraView.swift new file mode 100644 index 0000000..712f87d --- /dev/null +++ b/Memola/Components/Views/CameraView/CameraView.swift @@ -0,0 +1,44 @@ +// +// CameraView.swift +// Memola +// +// Created by Dscyre Scotti on 6/15/24. +// + +import SwiftUI + +struct CameraView: UIViewControllerRepresentable { + @Binding var image: UIImage? + + @Environment(\.dismiss) private var dismiss + + func makeUIViewController(context: Context) -> UIImagePickerController { + let picker = UIImagePickerController() + picker.sourceType = .camera + picker.delegate = context.coordinator + return picker + } + + func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) { } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate { + let parent: CameraView + + init(_ parent: CameraView) { + self.parent = parent + } + + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { + parent.image = info[.originalImage] as? UIImage + parent.dismiss() + } + + func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { + parent.dismiss() + } + } +} diff --git a/Memola/Config/Info.plist b/Memola/Config/Info.plist index 11d6956..5c10a78 100644 --- a/Memola/Config/Info.plist +++ b/Memola/Config/Info.plist @@ -2,6 +2,8 @@ + NSCameraUsageDescription + Memola requires access to the camera to capture photos. CFBundleShortVersionString $(MARKETING_VERSION) UISupportedInterfaceOrientations~ipad diff --git a/Memola/Features/Memo/Toolbar/Toolbar.swift b/Memola/Features/Memo/Toolbar/Toolbar.swift index 446096c..f09d011 100644 --- a/Memola/Features/Memo/Toolbar/Toolbar.swift +++ b/Memola/Features/Memo/Toolbar/Toolbar.swift @@ -8,6 +8,7 @@ import SwiftUI import PhotosUI import Foundation +import AVFoundation struct Toolbar: View { @Environment(\.dismiss) var dismiss @@ -19,6 +20,8 @@ struct Toolbar: View { @State var title: String @State var memo: MemoObject @State var photoItem: PhotosPickerItem? + @State var opensCamera: Bool = false + @State var isCameraAccessDenied: Bool = false @FocusState var textFieldState: Bool @@ -66,6 +69,22 @@ struct Toolbar: View { } } } + .fullScreenCover(isPresented: $opensCamera) { + CameraView(image: $tool.selectedImage) + .ignoresSafeArea() + } + .alert("Camera Access Denied", isPresented: $isCameraAccessDenied) { + Button { + if let url = URL(string: UIApplication.openSettingsURLString + "&path=CAMERA/\(String(describing: Bundle.main.bundleIdentifier))") { + UIApplication.shared.open(url) + } + } label: { + Text("Open Settings") + } + Button("Cancel", role: .cancel) { } + } message: { + Text("Memola requires access to the camera to capture photos. Please open Settings and enable camera access.") + } } var closeButton: some View { @@ -114,6 +133,7 @@ struct Toolbar: View { } } label: { Image(systemName: "pencil") + .fontWeight(.heavy) .contentShape(.circle) .frame(width: size, height: size) .background(tool.selection == .pen ? Color.accentColor : Color.clear) @@ -138,7 +158,7 @@ struct Toolbar: View { if tool.selection == .photo { HStack(spacing: 0) { Button { - + openCamera() } label: { Image(systemName: "camera.fill") .contentShape(.circle) @@ -175,7 +195,6 @@ struct Toolbar: View { history.historyPublisher.send(.undo) } label: { Image(systemName: "arrow.uturn.backward.circle") - .contentShape(.circle) } .hoverEffect(.lift) @@ -220,6 +239,26 @@ struct Toolbar: View { .hoverEffect(.lift) } + func openCamera() { + let status = AVCaptureDevice.authorizationStatus(for: .video) + switch status { + case .notDetermined: + AVCaptureDevice.requestAccess(for: .video) { status in + withAnimation { + if status { + opensCamera = true + } else { + isCameraAccessDenied = true + } + } + } + case .authorized: + opensCamera = true + default: + isCameraAccessDenied = true + } + } + func closeMemo() { withAnimation { canvas.state = .closing