feat: add pen tool persistence

This commit is contained in:
dscyrescotti
2024-05-17 23:30:32 +07:00
parent 3204328e5e
commit 4701eac3ba
13 changed files with 212 additions and 75 deletions
+39 -12
View File
@@ -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)
}
}
}
+32 -23
View File
@@ -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
}
}
@@ -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
}
}
}
@@ -183,6 +183,7 @@ extension CanvasViewController {
extension CanvasViewController {
func loadMemo() {
tool.load()
canvas.load()
}
+4 -9
View File
@@ -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()
}
@@ -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)
}
}
}
}
}
@@ -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)
+35 -18
View File
@@ -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)
}
}
}
+2 -1
View File
@@ -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
}
@@ -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<Persistence, NSManagedObjectContext>, 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
}
}
@@ -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?
}
@@ -15,6 +15,15 @@
<attribute name="title" attributeType="String"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="canvas" maxCount="1" deletionRule="Cascade" destinationEntity="CanvasObject" inverseName="memo" inverseEntity="CanvasObject"/>
<relationship name="tool" maxCount="1" deletionRule="Cascade" destinationEntity="ToolObject" inverseName="memo" inverseEntity="ToolObject"/>
</entity>
<entity name="PenObject" representedClassName="PenObject" syncable="YES">
<attribute name="color" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/>
<attribute name="isSelected" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="orderIndex" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="style" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="thickness" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<relationship name="tool" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ToolObject" inverseName="pens" inverseEntity="ToolObject"/>
</entity>
<entity name="QuadObject" representedClassName="QuadObject" syncable="YES">
<attribute name="color" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/>
@@ -34,4 +43,8 @@
<relationship name="graphicContext" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="GraphicContextObject" inverseName="strokes" inverseEntity="GraphicContextObject"/>
<relationship name="quads" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="QuadObject" inverseName="stroke" inverseEntity="QuadObject"/>
</entity>
<entity name="ToolObject" representedClassName="ToolObject" syncable="YES">
<relationship name="memo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MemoObject" inverseName="tool" inverseEntity="MemoObject"/>
<relationship name="pens" toMany="YES" deletionRule="Cascade" destinationEntity="PenObject" inverseName="tool" inverseEntity="PenObject"/>
</entity>
</model>