mirror of
https://github.com/dscyrescotti/Memola.git
synced 2026-04-17 22:39:48 +02:00
feat: bookmark image file for secure access
This commit is contained in:
@@ -86,6 +86,7 @@
|
|||||||
ECBE52962C1D5900006BDB3D /* PhotoPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECBE52952C1D5900006BDB3D /* PhotoPreview.swift */; };
|
ECBE52962C1D5900006BDB3D /* PhotoPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECBE52952C1D5900006BDB3D /* PhotoPreview.swift */; };
|
||||||
ECBE529C2C1D94A4006BDB3D /* CameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECBE529A2C1D94A4006BDB3D /* CameraView.swift */; };
|
ECBE529C2C1D94A4006BDB3D /* CameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECBE529A2C1D94A4006BDB3D /* CameraView.swift */; };
|
||||||
ECBE529E2C1DAB21006BDB3D /* UIImage++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECBE529D2C1DAB21006BDB3D /* UIImage++.swift */; };
|
ECBE529E2C1DAB21006BDB3D /* UIImage++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECBE529D2C1DAB21006BDB3D /* UIImage++.swift */; };
|
||||||
|
ECC995A32C1E8F2800B2699A /* PhotoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECC995A22C1E8F2800B2699A /* PhotoItem.swift */; };
|
||||||
ECD12A862C19EE3900B96E12 /* ElementObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A852C19EE3900B96E12 /* ElementObject.swift */; };
|
ECD12A862C19EE3900B96E12 /* ElementObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A852C19EE3900B96E12 /* ElementObject.swift */; };
|
||||||
ECD12A8A2C19EFB000B96E12 /* Element.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A892C19EFB000B96E12 /* Element.swift */; };
|
ECD12A8A2C19EFB000B96E12 /* Element.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A892C19EFB000B96E12 /* Element.swift */; };
|
||||||
ECD12A8C2C1AEAA900B96E12 /* PhotoObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A8B2C1AEAA900B96E12 /* PhotoObject.swift */; };
|
ECD12A8C2C1AEAA900B96E12 /* PhotoObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A8B2C1AEAA900B96E12 /* PhotoObject.swift */; };
|
||||||
@@ -187,6 +188,7 @@
|
|||||||
ECBE52952C1D5900006BDB3D /* PhotoPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPreview.swift; sourceTree = "<group>"; };
|
ECBE52952C1D5900006BDB3D /* PhotoPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPreview.swift; sourceTree = "<group>"; };
|
||||||
ECBE529A2C1D94A4006BDB3D /* CameraView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraView.swift; sourceTree = "<group>"; };
|
ECBE529A2C1D94A4006BDB3D /* CameraView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraView.swift; sourceTree = "<group>"; };
|
||||||
ECBE529D2C1DAB21006BDB3D /* UIImage++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage++.swift"; sourceTree = "<group>"; };
|
ECBE529D2C1DAB21006BDB3D /* UIImage++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage++.swift"; sourceTree = "<group>"; };
|
||||||
|
ECC995A22C1E8F2800B2699A /* PhotoItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoItem.swift; sourceTree = "<group>"; };
|
||||||
ECD12A852C19EE3900B96E12 /* ElementObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementObject.swift; sourceTree = "<group>"; };
|
ECD12A852C19EE3900B96E12 /* ElementObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementObject.swift; sourceTree = "<group>"; };
|
||||||
ECD12A892C19EFB000B96E12 /* Element.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Element.swift; sourceTree = "<group>"; };
|
ECD12A892C19EFB000B96E12 /* Element.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Element.swift; sourceTree = "<group>"; };
|
||||||
ECD12A8B2C1AEAA900B96E12 /* PhotoObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoObject.swift; sourceTree = "<group>"; };
|
ECD12A8B2C1AEAA900B96E12 /* PhotoObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoObject.swift; sourceTree = "<group>"; };
|
||||||
@@ -678,6 +680,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
ECBE52952C1D5900006BDB3D /* PhotoPreview.swift */,
|
ECBE52952C1D5900006BDB3D /* PhotoPreview.swift */,
|
||||||
|
ECC995A22C1E8F2800B2699A /* PhotoItem.swift */,
|
||||||
);
|
);
|
||||||
path = PhotoPreview;
|
path = PhotoPreview;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -926,6 +929,7 @@
|
|||||||
ECD12A8A2C19EFB000B96E12 /* Element.swift in Sources */,
|
ECD12A8A2C19EFB000B96E12 /* Element.swift in Sources */,
|
||||||
EC0D14282BF7BF20009BFE5F /* ContextMenuViewModifier.swift in Sources */,
|
EC0D14282BF7BF20009BFE5F /* ContextMenuViewModifier.swift in Sources */,
|
||||||
EC2106AD2C10C2A700FBE27C /* AnyStroke.swift in Sources */,
|
EC2106AD2C10C2A700FBE27C /* AnyStroke.swift in Sources */,
|
||||||
|
ECC995A32C1E8F2800B2699A /* PhotoItem.swift in Sources */,
|
||||||
ECA738BC2BE60E0300A4542E /* Tool.swift in Sources */,
|
ECA738BC2BE60E0300A4542E /* Tool.swift in Sources */,
|
||||||
ECA738972BE6014200A4542E /* Graphic.metal in Sources */,
|
ECA738972BE6014200A4542E /* Graphic.metal in Sources */,
|
||||||
ECA7388A2BE6006A00A4542E /* PipelineStates.swift in Sources */,
|
ECA7388A2BE6006A00A4542E /* PipelineStates.swift in Sources */,
|
||||||
|
|||||||
@@ -304,11 +304,11 @@ extension GraphicContext {
|
|||||||
|
|
||||||
// MARK: - Photo
|
// MARK: - Photo
|
||||||
extension GraphicContext {
|
extension GraphicContext {
|
||||||
func insertPhoto(at point: CGPoint, url: URL) {
|
func insertPhoto(at point: CGPoint, photoItem: PhotoItem) {
|
||||||
let size = CGSize(width: 100, height: 100)
|
let size = CGSize(width: 100, height: 100)
|
||||||
let origin = point
|
let origin = point
|
||||||
let bounds = [origin.x - size.width / 2, origin.y - size.height / 2, origin.x + size.width / 2, origin.y + size.height / 2]
|
let bounds = [origin.x - size.width / 2, origin.y - size.height / 2, origin.x + size.width / 2, origin.y + size.height / 2]
|
||||||
let photo = Photo(url: url, size: size, origin: origin, bounds: bounds, createdAt: .now)
|
let photo = Photo(url: photoItem.id, size: size, origin: origin, bounds: bounds, createdAt: .now, bookmark: photoItem.bookmark)
|
||||||
tree.insert(photo.element, in: photo.photoBox)
|
tree.insert(photo.element, in: photo.photoBox)
|
||||||
withPersistence(\.backgroundContext) { [_photo = photo, graphicContext = object] context in
|
withPersistence(\.backgroundContext) { [_photo = photo, graphicContext = object] context in
|
||||||
let photo = PhotoObject(\.backgroundContext)
|
let photo = PhotoObject(\.backgroundContext)
|
||||||
@@ -319,6 +319,7 @@ extension GraphicContext {
|
|||||||
photo.originX = _photo.origin.x
|
photo.originX = _photo.origin.x
|
||||||
photo.height = _photo.size.height
|
photo.height = _photo.size.height
|
||||||
photo.createdAt = _photo.createdAt
|
photo.createdAt = _photo.createdAt
|
||||||
|
photo.bookmark = _photo.bookmark
|
||||||
let element = ElementObject(\.backgroundContext)
|
let element = ElementObject(\.backgroundContext)
|
||||||
element.createdAt = _photo.createdAt
|
element.createdAt = _photo.createdAt
|
||||||
element.type = 1
|
element.type = 1
|
||||||
|
|||||||
@@ -128,20 +128,26 @@ extension Canvas {
|
|||||||
|
|
||||||
// MARK: - Photo
|
// MARK: - Photo
|
||||||
extension Canvas {
|
extension Canvas {
|
||||||
func insertPhoto(at point: CGPoint, url: URL) {
|
func insertPhoto(at point: CGPoint, photoItem: PhotoItem) {
|
||||||
graphicContext.insertPhoto(at: point, url: url)
|
graphicContext.insertPhoto(at: point, photoItem: photoItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
func savePhoto(_ data: Data) -> URL? {
|
func bookmarkPhoto(of image: UIImage) -> PhotoItem? {
|
||||||
|
guard let data = image.jpegData(compressionQuality: 1) else { return nil }
|
||||||
let fileManager = FileManager.default
|
let fileManager = FileManager.default
|
||||||
guard let directory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else { return nil }
|
guard let directory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
let fileName = "\(UUID().uuidString)-\(Date.now.timeIntervalSince1970)"
|
let fileName = "\(UUID().uuidString)-\(Date.now.timeIntervalSince1970)"
|
||||||
let folder = directory.appendingPathComponent(canvasID.uriRepresentation().lastPathComponent, conformingTo: .folder)
|
let folder = directory.appendingPathComponent(canvasID.uriRepresentation().lastPathComponent, conformingTo: .folder)
|
||||||
if !fileManager.fileExists(atPath: folder.path()) {
|
|
||||||
|
if folder.startAccessingSecurityScopedResource(), !fileManager.fileExists(atPath: folder.path()) {
|
||||||
do {
|
do {
|
||||||
try fileManager.createDirectory(at: folder, withIntermediateDirectories: true)
|
try fileManager.createDirectory(at: folder, withIntermediateDirectories: true)
|
||||||
|
folder.stopAccessingSecurityScopedResource()
|
||||||
} catch {
|
} catch {
|
||||||
NSLog("[Memola] - \(error.localizedDescription)")
|
NSLog("[Memola] - \(error.localizedDescription)")
|
||||||
|
folder.stopAccessingSecurityScopedResource()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,7 +158,14 @@ extension Canvas {
|
|||||||
NSLog("[Memola] - \(error.localizedDescription)")
|
NSLog("[Memola] - \(error.localizedDescription)")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return file
|
var photoBookmark: PhotoItem?
|
||||||
|
do {
|
||||||
|
let bookmark = try file.bookmarkData(options: .minimalBookmark)
|
||||||
|
photoBookmark = PhotoItem(id: file, image: image, bookmark: bookmark)
|
||||||
|
} catch {
|
||||||
|
NSLog("[Memola] - \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
return photoBookmark
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ final class Photo: @unchecked Sendable, Equatable, Comparable {
|
|||||||
var url: URL?
|
var url: URL?
|
||||||
var bounds: [CGFloat]
|
var bounds: [CGFloat]
|
||||||
var createdAt: Date
|
var createdAt: Date
|
||||||
|
var bookmark: Data?
|
||||||
|
|
||||||
var object: PhotoObject?
|
var object: PhotoObject?
|
||||||
|
|
||||||
@@ -24,12 +25,13 @@ final class Photo: @unchecked Sendable, Equatable, Comparable {
|
|||||||
var vertexCount: Int = 0
|
var vertexCount: Int = 0
|
||||||
var vertexBuffer: MTLBuffer?
|
var vertexBuffer: MTLBuffer?
|
||||||
|
|
||||||
init(url: URL?, size: CGSize, origin: CGPoint, bounds: [CGFloat], createdAt: Date) {
|
init(url: URL?, size: CGSize, origin: CGPoint, bounds: [CGFloat], createdAt: Date, bookmark: Data?) {
|
||||||
self.size = size
|
self.size = size
|
||||||
self.origin = origin
|
self.origin = origin
|
||||||
self.url = url
|
self.url = url
|
||||||
self.bounds = bounds
|
self.bounds = bounds
|
||||||
self.createdAt = createdAt
|
self.createdAt = createdAt
|
||||||
|
self.bookmark = bookmark
|
||||||
generateVertices()
|
generateVertices()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +41,8 @@ final class Photo: @unchecked Sendable, Equatable, Comparable {
|
|||||||
size: .init(width: object.width, height: object.height),
|
size: .init(width: object.width, height: object.height),
|
||||||
origin: .init(x: object.originX, y: object.originY),
|
origin: .init(x: object.originX, y: object.originY),
|
||||||
bounds: object.bounds,
|
bounds: object.bounds,
|
||||||
createdAt: object.createdAt ?? .now
|
createdAt: object.createdAt ?? .now,
|
||||||
|
bookmark: object.bookmark
|
||||||
)
|
)
|
||||||
self.object = object
|
self.object = object
|
||||||
}
|
}
|
||||||
@@ -56,6 +59,17 @@ final class Photo: @unchecked Sendable, Equatable, Comparable {
|
|||||||
PhotoVertex(x: maxX, y: maxY, textCoord: CGPoint(x: 1, y: 1)),
|
PhotoVertex(x: maxX, y: maxY, textCoord: CGPoint(x: 1, y: 1)),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getBookmarkURL() -> URL? {
|
||||||
|
var isStale = false
|
||||||
|
guard let bookmark else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let bookmarkURL = try? URL(resolvingBookmarkData: bookmark, options: .withoutUI, relativeTo: nil, bookmarkDataIsStale: &isStale) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return bookmarkURL
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Photo: Drawable {
|
extension Photo: Drawable {
|
||||||
@@ -64,7 +78,7 @@ extension Photo: Drawable {
|
|||||||
vertexCount = vertices.endIndex
|
vertexCount = vertices.endIndex
|
||||||
vertexBuffer = device.makeBuffer(bytes: vertices, length: vertexCount * MemoryLayout<PhotoVertex>.stride, options: [])
|
vertexBuffer = device.makeBuffer(bytes: vertices, length: vertexCount * MemoryLayout<PhotoVertex>.stride, options: [])
|
||||||
}
|
}
|
||||||
if texture == nil, let url {
|
if texture == nil, let url = getBookmarkURL() {
|
||||||
texture = Textures.createPhotoTexture(for: url, on: device)
|
texture = Textures.createPhotoTexture(for: url, on: device)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ public class Tool: NSObject, ObservableObject {
|
|||||||
@Published var selectedPen: Pen?
|
@Published var selectedPen: Pen?
|
||||||
@Published var draggedPen: Pen?
|
@Published var draggedPen: Pen?
|
||||||
// MARK: - Photo
|
// MARK: - Photo
|
||||||
@Published var selectedImageURL: URL?
|
@Published var selectedPhotoItem: PhotoItem?
|
||||||
|
|
||||||
@Published var selection: ToolSelection = .none
|
@Published var selection: ToolSelection = .none
|
||||||
|
|
||||||
|
|||||||
@@ -233,12 +233,12 @@ extension CanvasViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc func recognizeTapGesture(_ gesture: UITapGestureRecognizer) {
|
@objc func recognizeTapGesture(_ gesture: UITapGestureRecognizer) {
|
||||||
guard let url = tool.selectedImageURL else { return }
|
guard let photoItem = tool.selectedPhotoItem else { return }
|
||||||
withAnimation {
|
withAnimation {
|
||||||
tool.selectedImageURL = nil
|
tool.selectedPhotoItem = nil
|
||||||
}
|
}
|
||||||
let point = gesture.location(in: drawingView)
|
let point = gesture.location(in: drawingView)
|
||||||
canvas.insertPhoto(at: point.muliply(by: drawingView.ratio), url: url)
|
canvas.insertPhoto(at: point.muliply(by: drawingView.ratio), photoItem: photoItem)
|
||||||
drawingView.draw()
|
drawingView.draw()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct CameraView: UIViewControllerRepresentable {
|
struct CameraView: UIViewControllerRepresentable {
|
||||||
@Binding var url: URL?
|
@Binding var image: UIImage?
|
||||||
|
|
||||||
@ObservedObject var canvas: Canvas
|
@ObservedObject var canvas: Canvas
|
||||||
|
|
||||||
@@ -35,10 +35,7 @@ struct CameraView: UIViewControllerRepresentable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
|
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
|
||||||
let image = (info[.originalImage] as? UIImage)?.imageWithUpOrientation()
|
parent.image = (info[.originalImage] as? UIImage)?.imageWithUpOrientation()
|
||||||
if let image, let data = image.jpegData(compressionQuality: 1) {
|
|
||||||
parent.url = parent.canvas.savePhoto(data)
|
|
||||||
}
|
|
||||||
parent.dismiss()
|
parent.dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ struct MemoView: View {
|
|||||||
PenDock(tool: tool, canvas: canvas)
|
PenDock(tool: tool, canvas: canvas)
|
||||||
.transition(.move(edge: .trailing))
|
.transition(.move(edge: .trailing))
|
||||||
case .photo:
|
case .photo:
|
||||||
if let url = tool.selectedImageURL {
|
if let photoItem = tool.selectedPhotoItem {
|
||||||
PhotoPreview(url: url, tool: tool)
|
PhotoPreview(photoItem: photoItem, tool: tool)
|
||||||
.transition(.move(edge: .trailing))
|
.transition(.move(edge: .trailing))
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|||||||
15
Memola/Features/Memo/PhotoPreview/PhotoItem.swift
Normal file
15
Memola/Features/Memo/PhotoPreview/PhotoItem.swift
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
//
|
||||||
|
// PhotoItem.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 6/16/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct PhotoItem: Identifiable, Equatable {
|
||||||
|
var id: URL
|
||||||
|
let image: UIImage
|
||||||
|
let bookmark: Data
|
||||||
|
}
|
||||||
@@ -8,15 +8,11 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct PhotoPreview: View {
|
struct PhotoPreview: View {
|
||||||
let url: URL
|
let photoItem: PhotoItem
|
||||||
@ObservedObject var tool: Tool
|
@ObservedObject var tool: Tool
|
||||||
|
|
||||||
var data: Data {
|
|
||||||
(try? Data(contentsOf: url)) ?? Data()
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Image(uiImage: UIImage(data: data) ?? UIImage())
|
Image(uiImage: photoItem.image)
|
||||||
.resizable()
|
.resizable()
|
||||||
.scaledToFill()
|
.scaledToFill()
|
||||||
.frame(width: 100, height: 100)
|
.frame(width: 100, height: 100)
|
||||||
@@ -31,7 +27,7 @@ struct PhotoPreview: View {
|
|||||||
.overlay(alignment: .topLeading) {
|
.overlay(alignment: .topLeading) {
|
||||||
Button {
|
Button {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
tool.selectedImageURL = nil
|
tool.selectedPhotoItem = nil
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "xmark.circle.fill")
|
Image(systemName: "xmark.circle.fill")
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ struct Toolbar: View {
|
|||||||
|
|
||||||
@State var title: String
|
@State var title: String
|
||||||
@State var memo: MemoObject
|
@State var memo: MemoObject
|
||||||
@State var photoItem: PhotosPickerItem?
|
|
||||||
@State var opensCamera: Bool = false
|
@State var opensCamera: Bool = false
|
||||||
|
@State var photosPickerItem: PhotosPickerItem?
|
||||||
@State var isCameraAccessDenied: Bool = false
|
@State var isCameraAccessDenied: Bool = false
|
||||||
|
|
||||||
@FocusState var textFieldState: Bool
|
@FocusState var textFieldState: Bool
|
||||||
@@ -56,22 +56,28 @@ struct Toolbar: View {
|
|||||||
}
|
}
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.padding(10)
|
.padding(10)
|
||||||
.onChange(of: photoItem) { oldValue, newValue in
|
.onChange(of: photosPickerItem) { oldValue, newValue in
|
||||||
if newValue != nil {
|
if newValue != nil {
|
||||||
Task {
|
Task {
|
||||||
let data = try? await newValue?.loadTransferable(type: Data.self)
|
let data = try? await newValue?.loadTransferable(type: Data.self)
|
||||||
if let data {
|
if let data, let image = UIImage(data: data) {
|
||||||
let url = canvas.savePhoto(data)
|
let photoItem = canvas.bookmarkPhoto(of: image)
|
||||||
withAnimation {
|
withAnimation {
|
||||||
tool.selectedImageURL = url
|
tool.selectedPhotoItem = photoItem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
photoItem = nil
|
photosPickerItem = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.fullScreenCover(isPresented: $opensCamera) {
|
.fullScreenCover(isPresented: $opensCamera) {
|
||||||
CameraView(url: $tool.selectedImageURL, canvas: canvas)
|
let image: Binding<UIImage?> = Binding {
|
||||||
|
tool.selectedPhotoItem?.image
|
||||||
|
} set: { image in
|
||||||
|
guard let image else { return }
|
||||||
|
tool.selectedPhotoItem = canvas.bookmarkPhoto(of: image)
|
||||||
|
}
|
||||||
|
CameraView(image: image, canvas: canvas)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
}
|
}
|
||||||
.alert("Camera Access Denied", isPresented: $isCameraAccessDenied) {
|
.alert("Camera Access Denied", isPresented: $isCameraAccessDenied) {
|
||||||
@@ -167,7 +173,7 @@ struct Toolbar: View {
|
|||||||
.clipShape(.rect(cornerRadius: 8))
|
.clipShape(.rect(cornerRadius: 8))
|
||||||
}
|
}
|
||||||
.hoverEffect(.lift)
|
.hoverEffect(.lift)
|
||||||
PhotosPicker(selection: $photoItem, matching: .images, preferredItemEncoding: .compatible) {
|
PhotosPicker(selection: $photosPickerItem, matching: .images, preferredItemEncoding: .compatible) {
|
||||||
Image(systemName: "photo.fill.on.rectangle.fill")
|
Image(systemName: "photo.fill.on.rectangle.fill")
|
||||||
.contentShape(.circle)
|
.contentShape(.circle)
|
||||||
.frame(width: size, height: size)
|
.frame(width: size, height: size)
|
||||||
|
|||||||
@@ -17,5 +17,6 @@ class PhotoObject: NSManagedObject {
|
|||||||
@NSManaged var bounds: [CGFloat]
|
@NSManaged var bounds: [CGFloat]
|
||||||
@NSManaged var createdAt: Date?
|
@NSManaged var createdAt: Date?
|
||||||
@NSManaged var imageURL: URL?
|
@NSManaged var imageURL: URL?
|
||||||
|
@NSManaged var bookmark: Data?
|
||||||
@NSManaged var element: ElementObject?
|
@NSManaged var element: ElementObject?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
<relationship name="tool" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ToolObject" inverseName="pens" inverseEntity="ToolObject"/>
|
<relationship name="tool" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ToolObject" inverseName="pens" inverseEntity="ToolObject"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="PhotoObject" representedClassName="PhotoObject" syncable="YES">
|
<entity name="PhotoObject" representedClassName="PhotoObject" syncable="YES">
|
||||||
|
<attribute name="bookmark" optional="YES" attributeType="Binary"/>
|
||||||
<attribute name="bounds" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/>
|
<attribute name="bounds" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/>
|
||||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
<attribute name="height" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
<attribute name="height" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user