diff --git a/Memola.xcodeproj/project.pbxproj b/Memola.xcodeproj/project.pbxproj index 8991957..d527bc1 100644 --- a/Memola.xcodeproj/project.pbxproj +++ b/Memola.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + EC0D14212BF79C73009BFE5F /* ToolObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14202BF79C73009BFE5F /* ToolObject.swift */; }; + EC0D14242BF79C98009BFE5F /* MemolaModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14222BF79C98009BFE5F /* MemolaModel.xcdatamodeld */; }; + EC0D14262BF7A8C9009BFE5F /* PenObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14252BF7A8C9009BFE5F /* PenObject.swift */; }; EC3565522BEFC65F00A4E0BF /* NSManagedObjectContext++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3565512BEFC65F00A4E0BF /* NSManagedObjectContext++.swift */; }; EC3565542BEFC6AD00A4E0BF /* View++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3565532BEFC6AD00A4E0BF /* View++.swift */; }; EC3565562BEFC7B300A4E0BF /* NSManagedObject++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3565552BEFC7B300A4E0BF /* NSManagedObject++.swift */; }; @@ -70,7 +73,6 @@ ECA738F42BE612A000A4542E /* Array++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F32BE612A000A4542E /* Array++.swift */; }; ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F52BE612B700A4542E /* MTLDevice++.swift */; }; ECA738FC2BE61C5200A4542E /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738FB2BE61C5200A4542E /* Persistence.swift */; }; - ECA739012BE61D9C00A4542E /* MemolaModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = ECA738FF2BE61D9C00A4542E /* MemolaModel.xcdatamodeld */; }; ECA739082BE623F300A4542E /* PenToolView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA739072BE623F300A4542E /* PenToolView.swift */; }; ECEC01A82BEE11BA006DA24C /* QuadShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECEC01A72BEE11BA006DA24C /* QuadShape.swift */; }; ECFA15202BEF21EF00455818 /* MemoObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFA151F2BEF21EF00455818 /* MemoObject.swift */; }; @@ -81,6 +83,9 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + EC0D14202BF79C73009BFE5F /* ToolObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolObject.swift; sourceTree = ""; }; + EC0D14232BF79C98009BFE5F /* MemolaModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MemolaModel.xcdatamodel; sourceTree = ""; }; + EC0D14252BF7A8C9009BFE5F /* PenObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenObject.swift; sourceTree = ""; }; EC3565512BEFC65F00A4E0BF /* NSManagedObjectContext++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext++.swift"; sourceTree = ""; }; EC3565532BEFC6AD00A4E0BF /* View++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View++.swift"; sourceTree = ""; }; EC3565552BEFC7B300A4E0BF /* NSManagedObject++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObject++.swift"; sourceTree = ""; }; @@ -146,7 +151,6 @@ ECA738F32BE612A000A4542E /* Array++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array++.swift"; sourceTree = ""; }; ECA738F52BE612B700A4542E /* MTLDevice++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MTLDevice++.swift"; sourceTree = ""; }; ECA738FB2BE61C5200A4542E /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; - ECA739002BE61D9C00A4542E /* MemolaModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MemolaModel.xcdatamodel; sourceTree = ""; }; ECA739072BE623F300A4542E /* PenToolView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenToolView.swift; sourceTree = ""; }; ECEC01A72BEE11BA006DA24C /* QuadShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuadShape.swift; sourceTree = ""; }; ECFA151F2BEF21EF00455818 /* MemoObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoObject.swift; sourceTree = ""; }; @@ -556,7 +560,7 @@ ECA738FE2BE61D5700A4542E /* Models */ = { isa = PBXGroup; children = ( - ECA738FF2BE61D9C00A4542E /* MemolaModel.xcdatamodeld */, + EC0D14222BF79C98009BFE5F /* MemolaModel.xcdatamodeld */, ); path = Models; sourceTree = ""; @@ -586,6 +590,8 @@ ECFA15232BEF223300455818 /* GraphicContextObject.swift */, ECFA15252BEF224900455818 /* StrokeObject.swift */, ECFA15272BEF225000455818 /* QuadObject.swift */, + EC0D14202BF79C73009BFE5F /* ToolObject.swift */, + EC0D14252BF7A8C9009BFE5F /* PenObject.swift */, ); path = Objects; sourceTree = ""; @@ -696,8 +702,9 @@ ECA738EE2BE6125D00A4542E /* simd_float4x4++.swift in Sources */, ECA7388C2BE6009600A4542E /* Textures.swift in Sources */, ECA738B82BE60DDC00A4542E /* HistoryEvent.swift in Sources */, + EC0D14262BF7A8C9009BFE5F /* PenObject.swift in Sources */, ECA738952BE6012D00A4542E /* ViewPort.metal in Sources */, - ECA739012BE61D9C00A4542E /* MemolaModel.xcdatamodeld in Sources */, + EC0D14242BF79C98009BFE5F /* MemolaModel.xcdatamodeld in Sources */, ECA738F02BE6127700A4542E /* CGSize++.swift in Sources */, ECFA15242BEF223300455818 /* GraphicContextObject.swift in Sources */, EC3565562BEFC7B300A4E0BF /* NSManagedObject++.swift in Sources */, @@ -710,6 +717,7 @@ ECA7388F2BE600DA00A4542E /* Grid.metal in Sources */, ECA738C92BE60EF700A4542E /* GraphicContext.swift in Sources */, ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */, + EC0D14212BF79C73009BFE5F /* ToolObject.swift in Sources */, EC50500D2BF6674400B4D86E /* DraggableViewModifier.swift in Sources */, ECA7389E2BE601CB00A4542E /* QuadVertex.swift in Sources */, ECA738B32BE60D9E00A4542E /* CanvasView.swift in Sources */, @@ -944,13 +952,14 @@ /* End XCConfigurationList section */ /* Begin XCVersionGroup section */ - ECA738FF2BE61D9C00A4542E /* MemolaModel.xcdatamodeld */ = { + EC0D14222BF79C98009BFE5F /* MemolaModel.xcdatamodeld */ = { isa = XCVersionGroup; children = ( - ECA739002BE61D9C00A4542E /* MemolaModel.xcdatamodel */, + EC0D14232BF79C98009BFE5F /* MemolaModel.xcdatamodel */, ); - currentVersion = ECA739002BE61D9C00A4542E /* MemolaModel.xcdatamodel */; - path = MemolaModel.xcdatamodeld; + currentVersion = EC0D14232BF79C98009BFE5F /* MemolaModel.xcdatamodel */; + name = MemolaModel.xcdatamodeld; + path = /Users/dscyrescotti/Documents/Projects/Memola/Memola/Resources/Models/MemolaModel.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; }; diff --git a/Memola/Canvas/Tool/Core/Tool.swift b/Memola/Canvas/Tool/Core/Tool.swift index 6ec3144..db49795 100644 --- a/Memola/Canvas/Tool/Core/Tool.swift +++ b/Memola/Canvas/Tool/Core/Tool.swift @@ -6,27 +6,54 @@ // import SwiftUI +import CoreData import Foundation -class Tool: NSObject, ObservableObject { - @Published var pens: [Pen] +public class Tool: NSObject, ObservableObject { + let object: ToolObject + + @Published var pens: [Pen] = [] @Published var selectedPen: Pen? @Published var draggedPen: Pen? - override init() { - pens = [ - Pen(for: .eraser), - Pen(for: .marker) - ] - super.init() - selectedPen = pens[1] + init(object: ToolObject) { + self.object = object } - func changePen(_ pen: Pen) { - selectedPen = pen + func load() { + DispatchQueue.main.async { [weak self] in + guard let self else { return } + pens = object.pens.sortedArray(using: [NSSortDescriptor(key: "orderIndex", ascending: true)]).compactMap { + guard let pen = $0 as? PenObject else { return nil } + return Pen(object: pen) + } + if let selectedPen = pens.first(where: { $0.isSelected }) { + selectPen(selectedPen) + } + } + } + + func selectPen(_ pen: Pen) { + if let selectedPen { + unselectPen(selectedPen) + } + withAnimation { + selectedPen = pen + } + selectedPen?.isSelected = true + } + + func unselectPen(_ pen: Pen) { + pen.isSelected = false } func addPen(_ pen: Pen) { - pens.append(pen) + withAnimation { + pens.append(pen) + } + selectPen(pen) + if let _pen = pen.object { + object.pens.add(_pen) + } } } diff --git a/Memola/Canvas/Tool/Pen/Core/Pen.swift b/Memola/Canvas/Tool/Pen/Core/Pen.swift index e4b9a89..01a6b8a 100644 --- a/Memola/Canvas/Tool/Pen/Core/Pen.swift +++ b/Memola/Canvas/Tool/Pen/Core/Pen.swift @@ -10,32 +10,41 @@ import Foundation import UniformTypeIdentifiers class Pen: NSObject, ObservableObject, Identifiable { - let id: String - @Published var style: any PenStyle - @Published var color: [CGFloat] - @Published var thickness: CGFloat + var object: PenObject? - init(style: any PenStyle, color: [CGFloat], thickness: CGFloat) { - self.id = UUID().uuidString - self.style = style - self.color = color - self.thickness = thickness + let id: String + @Published var style: any PenStyle { + didSet { + object?.style = strokeStyle.rawValue + } + } + @Published var color: [CGFloat] { + didSet { + object?.color = color + } + } + @Published var thickness: CGFloat { + didSet { + object?.thickness = thickness + } + } + @Published var isSelected: Bool { + didSet { + object?.isSelected = isSelected + } + } + + init(object: PenObject) { + self.object = object + self.id = object.objectID.uriRepresentation().absoluteString + self.style = (Stroke.Style(rawValue: object.style) ?? .marker).anyPenStyle + self.color = object.color + self.thickness = object.thickness + self.isSelected = object.isSelected + super.init() } var strokeStyle: Stroke.Style { - switch style { - case is MarkerPenStyle: - return .marker - case is EraserPenStyle: - return .eraser - default: - return .marker - } - } -} - -extension Pen { - convenience init(for style: any PenStyle) { - self.init(style: style, color: style.color, thickness: style.thinkness.min) + style.strokeStyle } } diff --git a/Memola/Canvas/Tool/Pen/Core/PenStyle.swift b/Memola/Canvas/Tool/Pen/Core/PenStyle.swift index 618806c..a2a63ea 100644 --- a/Memola/Canvas/Tool/Pen/Core/PenStyle.swift +++ b/Memola/Canvas/Tool/Pen/Core/PenStyle.swift @@ -22,4 +22,15 @@ extension PenStyle { func loadTexture(on device: MTLDevice) -> MTLTexture? { Textures.createPenTexture(with: textureName, on: device) } + + var strokeStyle: Stroke.Style { + switch self { + case is MarkerPenStyle: + return .marker + case is EraserPenStyle: + return .eraser + default: + return .marker + } + } } diff --git a/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift index 0a37e68..50cc89a 100644 --- a/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift +++ b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift @@ -183,6 +183,7 @@ extension CanvasViewController { extension CanvasViewController { func loadMemo() { + tool.load() canvas.load() } diff --git a/Memola/Features/Memo/Memo/MemoView.swift b/Memola/Features/Memo/Memo/MemoView.swift index ed6e1f0..60a1a0c 100644 --- a/Memola/Features/Memo/Memo/MemoView.swift +++ b/Memola/Features/Memo/Memo/MemoView.swift @@ -10,9 +10,8 @@ import CoreData struct MemoView: View { @Environment(\.dismiss) var dismiss - @Environment(\.managedObjectContext) var managedObjectContext - @StateObject var tool = Tool() + @StateObject var tool: Tool @StateObject var canvas: Canvas @StateObject var history = History() @@ -20,6 +19,7 @@ struct MemoView: View { init(memo: MemoObject) { self.memo = memo + self._tool = StateObject(wrappedValue: Tool(object: memo.tool)) self._canvas = StateObject(wrappedValue: Canvas(size: memo.canvas.size, canvasID: memo.canvas.objectID)) } @@ -97,13 +97,8 @@ struct MemoView: View { } func closeMemo() { - history.resetRedo() - if managedObjectContext.hasChanges { - do { - try managedObjectContext.save() - } catch { - NSLog("[Memola] - \(error.localizedDescription)") - } + withPersistenceSync(\.viewContext) { context in + try context.saveIfNeeded() } dismiss() } diff --git a/Memola/Features/Memo/PenTool/PenDropDelegate.swift b/Memola/Features/Memo/PenTool/PenDropDelegate.swift index 972e83e..27bc4e2 100644 --- a/Memola/Features/Memo/PenTool/PenDropDelegate.swift +++ b/Memola/Features/Memo/PenTool/PenDropDelegate.swift @@ -27,6 +27,11 @@ struct PenDropDelegate: DropDelegate { tool.pens.move(fromOffsets: IndexSet(integer: fromIndex), toOffset: toIndex > fromIndex ? toIndex + 1 : toIndex) tool.objectWillChange.send() } + withPersistence(\.viewContext) { context in + for (index, pen) in tool.pens.enumerated() { + pen.object?.orderIndex = Int16(index) + } + } } } } diff --git a/Memola/Features/Memo/PenTool/PenToolView.swift b/Memola/Features/Memo/PenTool/PenToolView.swift index d79d558..353ba54 100644 --- a/Memola/Features/Memo/PenTool/PenToolView.swift +++ b/Memola/Features/Memo/PenTool/PenToolView.swift @@ -82,11 +82,11 @@ struct PenToolView: View { .onTapGesture { if tool.selectedPen === pen { withAnimation { - tool.selectedPen = nil + tool.unselectPen(pen) } } else { withAnimation { - tool.changePen(pen) + tool.selectPen(pen) } } } @@ -95,9 +95,13 @@ struct PenToolView: View { var newPenButton: some View { Button(action: { - let pen = Pen(for: .marker) + let pen = PenObject.createObject(\.viewContext, penStyle: .marker) pen.color = [Color.red, Color.blue, Color.green, Color.black, Color.orange].randomElement()!.components - tool.addPen(pen) + pen.isSelected = true + pen.tool = tool.object + pen.orderIndex = Int16(tool.pens.count) + let _pen = Pen(object: pen) + tool.addPen(_pen) }) { Image(systemName: "plus") .font(.title3) diff --git a/Memola/Features/Memos/MemosView.swift b/Memola/Features/Memos/MemosView.swift index 57c6395..4f66f0d 100644 --- a/Memola/Features/Memos/MemosView.swift +++ b/Memola/Features/Memos/MemosView.swift @@ -62,28 +62,45 @@ struct MemosView: View { } func createMemo(title: String) { - do { - let memoObject = MemoObject(context: managedObjectContext) - memoObject.title = title - memoObject.createdAt = .now - memoObject.updatedAt = .now + let memoObject = MemoObject(\.viewContext) + memoObject.title = title + memoObject.createdAt = .now + memoObject.updatedAt = .now - let canvasObject = CanvasObject(context: managedObjectContext) - canvasObject.width = 8_000 - canvasObject.height = 8_000 + let canvasObject = CanvasObject(context: managedObjectContext) + canvasObject.width = 8_000 + canvasObject.height = 8_000 - let graphicContextObject = GraphicContextObject(context: managedObjectContext) - graphicContextObject.strokes = [] + let toolObject = ToolObject(\.viewContext) + toolObject.pens = [] - memoObject.canvas = canvasObject - canvasObject.memo = memoObject - canvasObject.graphicContext = graphicContextObject - graphicContextObject.canvas = canvasObject + let eraserPenObject = PenObject.createObject(\.viewContext, penStyle: .eraser) + eraserPenObject.orderIndex = 0 + let markerPenObject = PenObject.createObject(\.viewContext, penStyle: .marker) + markerPenObject.orderIndex = 1 - try managedObjectContext.save() - openMemo(for: memoObject) - } catch { - NSLog("[Memola] - \(error.localizedDescription)") + let graphicContextObject = GraphicContextObject(\.viewContext) + graphicContextObject.strokes = [] + + memoObject.canvas = canvasObject + memoObject.tool = toolObject + + canvasObject.memo = memoObject + canvasObject.graphicContext = graphicContextObject + + toolObject.memo = memoObject + toolObject.pens = [eraserPenObject, markerPenObject] + + eraserPenObject.tool = toolObject + markerPenObject.tool = toolObject + + graphicContextObject.canvas = canvasObject + + withPersistenceSync(\.viewContext) { context in + try context.save() + DispatchQueue.main.async { + openMemo(for: memoObject) + } } } diff --git a/Memola/Persistence/Objects/MemoObject.swift b/Memola/Persistence/Objects/MemoObject.swift index d3a6b61..ad74aa9 100644 --- a/Memola/Persistence/Objects/MemoObject.swift +++ b/Memola/Persistence/Objects/MemoObject.swift @@ -10,9 +10,10 @@ import Foundation @objc(MemoObject) final class MemoObject: NSManagedObject, Identifiable { - @NSManaged var title: String @NSManaged var data: Data + @NSManaged var title: String @NSManaged var createdAt: Date @NSManaged var updatedAt: Date + @NSManaged var tool: ToolObject @NSManaged var canvas: CanvasObject } diff --git a/Memola/Persistence/Objects/PenObject.swift b/Memola/Persistence/Objects/PenObject.swift new file mode 100644 index 0000000..52380ec --- /dev/null +++ b/Memola/Persistence/Objects/PenObject.swift @@ -0,0 +1,30 @@ +// +// PenObject.swift +// Memola +// +// Created by Dscyre Scotti on 5/17/24. +// + +import CoreData +import Foundation + +@objc(PenObject) +class PenObject: NSManagedObject { + @NSManaged var color: [CGFloat] + @NSManaged var style: Int16 + @NSManaged var thickness: CGFloat + @NSManaged var isSelected: Bool + @NSManaged var orderIndex: Int16 + @NSManaged var tool: ToolObject? +} + +extension PenObject { + static func createObject(_ keyPath: KeyPath, penStyle: any PenStyle) -> PenObject { + let object = PenObject(context: Persistence.shared[keyPath: keyPath]) + object.color = penStyle.color + object.style = penStyle.strokeStyle.rawValue + object.isSelected = false + object.thickness = penStyle.thinkness.min + return object + } +} diff --git a/Memola/Persistence/Objects/ToolObject.swift b/Memola/Persistence/Objects/ToolObject.swift new file mode 100644 index 0000000..fb26c1a --- /dev/null +++ b/Memola/Persistence/Objects/ToolObject.swift @@ -0,0 +1,15 @@ +// +// ToolObject.swift +// Memola +// +// Created by Dscyre Scotti on 5/17/24. +// + +import CoreData +import Foundation + +@objc(ToolObject) +class ToolObject: NSManagedObject { + @NSManaged var pens: NSMutableSet + @NSManaged var memo: MemoObject? +} diff --git a/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents b/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents index 1b277fa..798206c 100644 --- a/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents +++ b/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents @@ -15,6 +15,15 @@ + + + + + + + + + @@ -34,4 +43,8 @@ + + + + \ No newline at end of file