feat: execute core data related logic in background context

This commit is contained in:
dscyrescotti
2024-05-11 21:30:58 +07:00
parent a903a5eed3
commit 10e7350511
21 changed files with 373 additions and 184 deletions
+28 -4
View File
@@ -64,9 +64,13 @@
ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F52BE612B700A4542E /* MTLDevice++.swift */; }; ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F52BE612B700A4542E /* MTLDevice++.swift */; };
ECA738FC2BE61C5200A4542E /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738FB2BE61C5200A4542E /* Persistence.swift */; }; ECA738FC2BE61C5200A4542E /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738FB2BE61C5200A4542E /* Persistence.swift */; };
ECA739012BE61D9C00A4542E /* MemolaModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = ECA738FF2BE61D9C00A4542E /* MemolaModel.xcdatamodeld */; }; ECA739012BE61D9C00A4542E /* MemolaModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = ECA738FF2BE61D9C00A4542E /* MemolaModel.xcdatamodeld */; };
ECA739052BE61E3100A4542E /* Memo.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA739042BE61E3100A4542E /* Memo.swift */; };
ECA739082BE623F300A4542E /* PenToolView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA739072BE623F300A4542E /* PenToolView.swift */; }; ECA739082BE623F300A4542E /* PenToolView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA739072BE623F300A4542E /* PenToolView.swift */; };
ECEC01A82BEE11BA006DA24C /* QuadShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECEC01A72BEE11BA006DA24C /* QuadShape.swift */; }; ECEC01A82BEE11BA006DA24C /* QuadShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECEC01A72BEE11BA006DA24C /* QuadShape.swift */; };
ECFA15202BEF21EF00455818 /* MemoObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFA151F2BEF21EF00455818 /* MemoObject.swift */; };
ECFA15222BEF21F500455818 /* CanvasObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFA15212BEF21F500455818 /* CanvasObject.swift */; };
ECFA15242BEF223300455818 /* GraphicContextObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFA15232BEF223300455818 /* GraphicContextObject.swift */; };
ECFA15262BEF224900455818 /* StrokeObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFA15252BEF224900455818 /* StrokeObject.swift */; };
ECFA15282BEF225000455818 /* QuadObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFA15272BEF225000455818 /* QuadObject.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
@@ -128,9 +132,13 @@
ECA738F52BE612B700A4542E /* MTLDevice++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MTLDevice++.swift"; sourceTree = "<group>"; }; ECA738F52BE612B700A4542E /* MTLDevice++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MTLDevice++.swift"; sourceTree = "<group>"; };
ECA738FB2BE61C5200A4542E /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; }; ECA738FB2BE61C5200A4542E /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
ECA739002BE61D9C00A4542E /* MemolaModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MemolaModel.xcdatamodel; sourceTree = "<group>"; }; ECA739002BE61D9C00A4542E /* MemolaModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MemolaModel.xcdatamodel; sourceTree = "<group>"; };
ECA739042BE61E3100A4542E /* Memo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Memo.swift; sourceTree = "<group>"; };
ECA739072BE623F300A4542E /* PenToolView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenToolView.swift; sourceTree = "<group>"; }; ECA739072BE623F300A4542E /* PenToolView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenToolView.swift; sourceTree = "<group>"; };
ECEC01A72BEE11BA006DA24C /* QuadShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuadShape.swift; sourceTree = "<group>"; }; ECEC01A72BEE11BA006DA24C /* QuadShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuadShape.swift; sourceTree = "<group>"; };
ECFA151F2BEF21EF00455818 /* MemoObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoObject.swift; sourceTree = "<group>"; };
ECFA15212BEF21F500455818 /* CanvasObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CanvasObject.swift; sourceTree = "<group>"; };
ECFA15232BEF223300455818 /* GraphicContextObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphicContextObject.swift; sourceTree = "<group>"; };
ECFA15252BEF224900455818 /* StrokeObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StrokeObject.swift; sourceTree = "<group>"; };
ECFA15272BEF225000455818 /* QuadObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuadObject.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@@ -226,7 +234,6 @@
ECA7387B2BE5EF3500A4542E /* Memo */ = { ECA7387B2BE5EF3500A4542E /* Memo */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
ECA739042BE61E3100A4542E /* Memo.swift */,
ECA7387C2BE5EF4B00A4542E /* MemoView.swift */, ECA7387C2BE5EF4B00A4542E /* MemoView.swift */,
ECA739072BE623F300A4542E /* PenToolView.swift */, ECA739072BE623F300A4542E /* PenToolView.swift */,
); );
@@ -453,6 +460,7 @@
ECA738FA2BE61B1700A4542E /* Persistence */ = { ECA738FA2BE61B1700A4542E /* Persistence */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
ECFA151E2BEF21BE00455818 /* Objects */,
ECA739022BE61DE700A4542E /* Core */, ECA739022BE61DE700A4542E /* Core */,
); );
path = Persistence; path = Persistence;
@@ -491,6 +499,18 @@
path = Core; path = Core;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
ECFA151E2BEF21BE00455818 /* Objects */ = {
isa = PBXGroup;
children = (
ECFA151F2BEF21EF00455818 /* MemoObject.swift */,
ECFA15212BEF21F500455818 /* CanvasObject.swift */,
ECFA15232BEF223300455818 /* GraphicContextObject.swift */,
ECFA15252BEF224900455818 /* StrokeObject.swift */,
ECFA15272BEF225000455818 /* QuadObject.swift */,
);
path = Objects;
sourceTree = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
@@ -569,14 +589,17 @@
ECA7389C2BE601AF00A4542E /* GridVertex.swift in Sources */, ECA7389C2BE601AF00A4542E /* GridVertex.swift in Sources */,
ECA738A82BE6025900A4542E /* GraphicUniforms.swift in Sources */, ECA738A82BE6025900A4542E /* GraphicUniforms.swift in Sources */,
ECA738E62BE611FD00A4542E /* CGRect++.swift in Sources */, ECA738E62BE611FD00A4542E /* CGRect++.swift in Sources */,
ECFA15262BEF224900455818 /* StrokeObject.swift in Sources */,
ECA738E82BE6120F00A4542E /* Color++.swift in Sources */, ECA738E82BE6120F00A4542E /* Color++.swift in Sources */,
ECA738FC2BE61C5200A4542E /* Persistence.swift in Sources */, ECA738FC2BE61C5200A4542E /* Persistence.swift in Sources */,
ECA7387A2BE5EF0400A4542E /* MemosView.swift in Sources */, ECA7387A2BE5EF0400A4542E /* MemosView.swift in Sources */,
ECA738BA2BE60DEF00A4542E /* HistoryAction.swift in Sources */, ECA738BA2BE60DEF00A4542E /* HistoryAction.swift in Sources */,
ECFA15222BEF21F500455818 /* CanvasObject.swift in Sources */,
ECA738AA2BE6026D00A4542E /* Uniforms.swift in Sources */, ECA738AA2BE6026D00A4542E /* Uniforms.swift in Sources */,
ECA7387D2BE5EF4B00A4542E /* MemoView.swift in Sources */, ECA7387D2BE5EF4B00A4542E /* MemoView.swift in Sources */,
ECA738DA2BE60FF100A4542E /* CacheRenderPass.swift in Sources */, ECA738DA2BE60FF100A4542E /* CacheRenderPass.swift in Sources */,
ECA738CD2BE60F2F00A4542E /* GridContext.swift in Sources */, ECA738CD2BE60F2F00A4542E /* GridContext.swift in Sources */,
ECFA15202BEF21EF00455818 /* MemoObject.swift in Sources */,
ECA738C62BE60E9D00A4542E /* EraserPenStyle.swift in Sources */, ECA738C62BE60E9D00A4542E /* EraserPenStyle.swift in Sources */,
ECA738EA2BE6122E00A4542E /* CGPoint++.swift in Sources */, ECA738EA2BE6122E00A4542E /* CGPoint++.swift in Sources */,
ECA738A62BE6023F00A4542E /* GridUniforms.swift in Sources */, ECA738A62BE6023F00A4542E /* GridUniforms.swift in Sources */,
@@ -594,11 +617,11 @@
ECA738952BE6012D00A4542E /* ViewPort.metal in Sources */, ECA738952BE6012D00A4542E /* ViewPort.metal in Sources */,
ECA739012BE61D9C00A4542E /* MemolaModel.xcdatamodeld in Sources */, ECA739012BE61D9C00A4542E /* MemolaModel.xcdatamodeld in Sources */,
ECA738F02BE6127700A4542E /* CGSize++.swift in Sources */, ECA738F02BE6127700A4542E /* CGSize++.swift in Sources */,
ECFA15242BEF223300455818 /* GraphicContextObject.swift in Sources */,
ECA738EC2BE6124E00A4542E /* CGAffineTransform++.swift in Sources */, ECA738EC2BE6124E00A4542E /* CGAffineTransform++.swift in Sources */,
ECA738E22BE610D000A4542E /* GraphicRenderPass.swift in Sources */, ECA738E22BE610D000A4542E /* GraphicRenderPass.swift in Sources */,
ECA738DC2BE6108D00A4542E /* StrokeRenderPass.swift in Sources */, ECA738DC2BE6108D00A4542E /* StrokeRenderPass.swift in Sources */,
ECA738F42BE612A000A4542E /* Array++.swift in Sources */, ECA738F42BE612A000A4542E /* Array++.swift in Sources */,
ECA739052BE61E3100A4542E /* Memo.swift in Sources */,
EC4538892BEBCAE000A86FEC /* Quad.swift in Sources */, EC4538892BEBCAE000A86FEC /* Quad.swift in Sources */,
ECA7388F2BE600DA00A4542E /* Grid.metal in Sources */, ECA7388F2BE600DA00A4542E /* Grid.metal in Sources */,
ECA738C92BE60EF700A4542E /* GraphicContext.swift in Sources */, ECA738C92BE60EF700A4542E /* GraphicContext.swift in Sources */,
@@ -607,6 +630,7 @@
ECA738B32BE60D9E00A4542E /* CanvasView.swift in Sources */, ECA738B32BE60D9E00A4542E /* CanvasView.swift in Sources */,
ECA738C42BE60E8800A4542E /* MarkerPenStyle.swift in Sources */, ECA738C42BE60E8800A4542E /* MarkerPenStyle.swift in Sources */,
ECA738BF2BE60E3400A4542E /* Pen.swift in Sources */, ECA738BF2BE60E3400A4542E /* Pen.swift in Sources */,
ECFA15282BEF225000455818 /* QuadObject.swift in Sources */,
ECA738932BE6011100A4542E /* Stroke.metal in Sources */, ECA738932BE6011100A4542E /* Stroke.metal in Sources */,
ECA738B62BE60DCD00A4542E /* History.swift in Sources */, ECA738B62BE60DCD00A4542E /* History.swift in Sources */,
ECA738D22BE60F7B00A4542E /* Stroke.swift in Sources */, ECA738D22BE60F7B00A4542E /* Stroke.swift in Sources */,
+63 -29
View File
@@ -10,11 +10,9 @@ import MetalKit
import CoreData import CoreData
import Foundation import Foundation
@objc(GraphicContext) final class GraphicContext: @unchecked Sendable {
final class GraphicContext: NSManagedObject { var strokes: [Stroke] = []
@NSManaged var id: UUID var object: GraphicContextObject?
@NSManaged var canvas: Canvas?
@NSManaged var strokes: NSMutableOrderedSet
var currentStroke: Stroke? var currentStroke: Stroke?
var previousStroke: Stroke? var previousStroke: Stroke?
@@ -24,8 +22,7 @@ final class GraphicContext: NSManagedObject {
var vertexCount: Int = 4 var vertexCount: Int = 4
var vertexBuffer: MTLBuffer? var vertexBuffer: MTLBuffer?
override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) { init() {
super.init(entity: entity, insertInto: context)
setViewPortVertices() setViewPortVertices()
} }
@@ -40,21 +37,37 @@ final class GraphicContext: NSManagedObject {
} }
func undoGraphic() { func undoGraphic() {
guard let stroke = strokes.lastObject as? Stroke else { return } guard !strokes.isEmpty else { return }
strokes.remove(stroke) let stroke = strokes.removeLast()
stroke.graphicContext = nil Persistence.backgroundContext.perform {
stroke.object?.graphicContext = nil
Persistence.saveIfNeededInBackground()
}
previousStroke = nil previousStroke = nil
Persistence.saveIfNeeded()
} }
func redoGraphic(for event: HistoryEvent) { func redoGraphic(for event: HistoryEvent) {
switch event { switch event {
case .stroke(let stroke): case .stroke(let stroke):
strokes.add(stroke) strokes.append(stroke)
stroke.graphicContext = self Persistence.backgroundContext.perform { [weak self] in
stroke.object?.graphicContext = self?.object
Persistence.saveIfNeededInBackground()
}
previousStroke = nil previousStroke = nil
} }
Persistence.saveIfNeeded() }
}
extension GraphicContext {
func load() {
guard let object else { return }
self.strokes = object.strokes.compactMap { stroke -> Stroke? in
guard let stroke = stroke as? StrokeObject else { return nil }
let _stroke = Stroke(object: stroke)
_stroke.loadVertices()
return _stroke
}
} }
} }
@@ -76,15 +89,24 @@ extension GraphicContext: Drawable {
extension GraphicContext { extension GraphicContext {
func beginStroke(at point: CGPoint, pen: Pen) -> Stroke { func beginStroke(at point: CGPoint, pen: Pen) -> Stroke {
let stroke = Stroke(context: Persistence.context) let stroke = Stroke(
stroke.id = UUID() color: pen.color,
stroke.color = pen.color style: pen.strokeStyle.rawValue,
stroke.style = pen.strokeStyle.rawValue createdAt: .now,
stroke.thickness = pen.thickness thickness: pen.thickness
stroke.createdAt = .now )
stroke.quads = [] Persistence.backgroundContext.perform { [graphicContext = object, _stroke = stroke] in
stroke.graphicContext = self let stroke = StrokeObject(context: Persistence.backgroundContext)
strokes.add(stroke) stroke.color = _stroke.color
stroke.style = _stroke.style
stroke.thickness = _stroke.thickness
stroke.createdAt = _stroke.createdAt
stroke.quads = []
stroke.graphicContext = graphicContext
graphicContext?.strokes.add(stroke)
_stroke.object = stroke
}
strokes.append(stroke)
currentStroke = stroke currentStroke = stroke
currentPoint = point currentPoint = point
currentStroke?.begin(at: point) currentStroke?.begin(at: point)
@@ -101,19 +123,31 @@ extension GraphicContext {
func endStroke(at point: CGPoint) { func endStroke(at point: CGPoint) {
guard currentPoint != nil, let currentStroke else { return } guard currentPoint != nil, let currentStroke else { return }
currentStroke.finish(at: point) currentStroke.finish(at: point)
Persistence.saveIfNeeded() let saveIndex = currentStroke.batchIndex
let quads = Array(currentStroke.quads[saveIndex..<currentStroke.quads.count])
Persistence.backgroundContext.perform { [currentStroke, quads] in
currentStroke.saveQuads(for: quads)
Persistence.saveIfNeededInBackground()
if let stroke = currentStroke.object {
currentStroke.quads.removeAll()
Persistence.backgroundContext.refresh(stroke, mergeChanges: false)
}
}
previousStroke = currentStroke previousStroke = currentStroke
self.currentStroke = nil self.currentStroke = nil
self.currentPoint = nil self.currentPoint = nil
} }
func cancelStroke() { func cancelStroke() {
if let stroke = strokes.lastObject as? Stroke { if !strokes.isEmpty {
Persistence.performe { context in let stroke = strokes.removeLast()
strokes.remove(stroke) Persistence.backgroundContext.perform { [graphicContext = object, _stroke = stroke] in
context.delete(stroke) if let stroke = _stroke.object {
graphicContext?.strokes.remove(stroke)
Persistence.backgroundContext.delete(stroke)
}
Persistence.saveIfNeededInBackground()
} }
Persistence.saveIfNeeded()
} }
currentStroke = nil currentStroke = nil
currentPoint = nil currentPoint = nil
+24 -17
View File
@@ -10,16 +10,14 @@ import CoreData
import MetalKit import MetalKit
import Foundation import Foundation
@objc(Canvas) final class Canvas: ObservableObject, Identifiable, @unchecked Sendable {
final class Canvas: NSManagedObject, Identifiable { let size: CGSize
@NSManaged var id: UUID let canvasID: NSManagedObjectID
@NSManaged var width: CGFloat
@NSManaged var height: CGFloat
@NSManaged var memo: Memo?
@NSManaged var graphicContext: GraphicContext
let gridContext = GridContext() let gridContext = GridContext()
var graphicContext = GraphicContext()
let viewPortContext = ViewPortContext() let viewPortContext = ViewPortContext()
let maximumZoomScale: CGFloat = 28 let maximumZoomScale: CGFloat = 28
let minimumZoomScale: CGFloat = 3.1 let minimumZoomScale: CGFloat = 3.1
@@ -28,9 +26,13 @@ final class Canvas: NSManagedObject, Identifiable {
var zoomScale: CGFloat = .zero var zoomScale: CGFloat = .zero
var uniformsBuffer: MTLBuffer? var uniformsBuffer: MTLBuffer?
init(size: CGSize, canvasID: NSManagedObjectID) {
self.size = size
self.canvasID = canvasID
}
@Published var state: State = .initial @Published var state: State = .initial
var size: CGSize { CGSize(width: width, height: height) }
var hasValidStroke: Bool { var hasValidStroke: Bool {
if let currentStroke = graphicContext.currentStroke { if let currentStroke = graphicContext.currentStroke {
return Date.now.timeIntervalSince(currentStroke.createdAt) * 1000 > 80 return Date.now.timeIntervalSince(currentStroke.createdAt) * 1000 > 80
@@ -42,16 +44,21 @@ final class Canvas: NSManagedObject, Identifiable {
// MARK: - Actions // MARK: - Actions
extension Canvas { extension Canvas {
func load() { func load() {
state = .loading Persistence.backgroundContext.perform { [weak self, canvasID] in
let start = Date().formatted(.dateTime.minute().second().secondFraction(.fractional(5))) DispatchQueue.main.async { [weak self] in
for stroke in graphicContext.strokes { self?.state = .loading
if let stroke = stroke as? Stroke { }
stroke.loadVertices() guard let canvas = Persistence.backgroundContext.object(with: canvasID) as? CanvasObject else {
return
}
let graphicContext = canvas.graphicContext
self?.graphicContext.object = graphicContext
self?.graphicContext.load()
Persistence.backgroundContext.refresh(canvas, mergeChanges: false)
DispatchQueue.main.async { [weak self] in
self?.state = .loaded
} }
} }
let end = Date().formatted(.dateTime.minute().second().secondFraction(.fractional(5)))
NSLog("[Memola] - Loaded from \(start) to \(end)")
state = .loaded
} }
} }
@@ -104,7 +111,7 @@ extension Canvas {
} }
func getNewlyAddedStroke() -> Stroke? { func getNewlyAddedStroke() -> Stroke? {
graphicContext.strokes.lastObject as? Stroke graphicContext.strokes.last
} }
} }
+22 -9
View File
@@ -8,15 +8,28 @@
import CoreData import CoreData
import Foundation import Foundation
@objc(Quad) struct Quad {
class Quad: NSManagedObject { var originX: CGFloat
@NSManaged var id: UUID var originY: CGFloat
@NSManaged var originX: CGFloat var size: CGFloat
@NSManaged var originY: CGFloat var rotation: CGFloat
@NSManaged var size: CGFloat var shape: Int16
@NSManaged var rotation: CGFloat
@NSManaged var shape: Int16 init(object: QuadObject) {
@NSManaged var stroke: Stroke? self.originX = object.originX
self.originY = object.originY
self.size = object.size
self.rotation = object.rotation
self.shape = object.shape
}
init(origin: CGPoint, size: CGFloat, rotation: CGFloat, shape: Int16) {
self.originX = origin.x
self.originY = origin.y
self.size = size
self.rotation = rotation
self.shape = shape
}
var origin: CGPoint { var origin: CGPoint {
get { CGPoint(x: originX, y: originY) } get { CGPoint(x: originX, y: originY) }
@@ -132,42 +132,15 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
} }
} }
#warning("TODO: remove later")
private func addLine(from start: CGPoint, to end: CGPoint, on stroke: Stroke) {
let distance = end.distance(to: start)
let segments = max(distance / stroke.penStyle.anyPenStyle.stepRate, 2)
for i in 0..<Int(segments) {
let i = CGFloat(i)
let x = start.x + (end.x - start.x) * (i / segments)
let y = start.y + (end.y - start.y) * (i / segments)
let point = CGPoint(x: x, y: y)
addPoint(point, on: stroke)
}
}
private func discardVertices(upto index: Int, quadIndex: Int, on stroke: Stroke) { private func discardVertices(upto index: Int, quadIndex: Int, on stroke: Stroke) {
if index < 0 { if index < 0 {
stroke.vertices.removeAll() stroke.vertices.removeAll()
discardQuads(from: quadIndex + 1, on: stroke)
} else { } else {
let count = stroke.vertices.endIndex let count = stroke.vertices.endIndex
let dropCount = count - (max(0, index) + 1) let dropCount = count - (max(0, index) + 1)
stroke.vertices.removeLast(dropCount) stroke.vertices.removeLast(dropCount)
discardQuads(from: quadIndex + 1, on: stroke)
}
}
private func discardQuads(from start: Int, on stroke: Stroke) {
let quads = stroke.quads.array
Persistence.performe { context in
for index in start..<quads.count {
if let quad = quads[index] as? Quad {
quad.stroke = nil
context.delete(quad)
stroke.quads.remove(quad)
}
}
} }
stroke.removeQuads(from: quadIndex + 1)
} }
} }
+66 -19
View File
@@ -9,15 +9,36 @@ import MetalKit
import CoreData import CoreData
import Foundation import Foundation
@objc(Stroke) final class Stroke: @unchecked Sendable {
final class Stroke: NSManagedObject { var object: StrokeObject?
@NSManaged var id: UUID var color: [CGFloat]
@NSManaged var color: [CGFloat] var style: Int16
@NSManaged var style: Int16 var createdAt: Date
@NSManaged var createdAt: Date var thickness: CGFloat
@NSManaged var thickness: CGFloat var quads: [Quad]
@NSManaged var quads: NSMutableOrderedSet
@NSManaged var graphicContext: GraphicContext? init(object: StrokeObject) {
self.object = object
self.color = object.color
self.style = object.style
self.createdAt = object.createdAt
self.thickness = object.thickness
self.quads = []
}
init(
color: [CGFloat],
style: Int16,
createdAt: Date,
thickness: CGFloat,
quads: [Quad] = []
) {
self.color = color
self.style = style
self.createdAt = createdAt
self.thickness = thickness
self.quads = quads
}
var angle: CGFloat = 0 var angle: CGFloat = 0
@@ -25,6 +46,7 @@ final class Stroke: NSManagedObject {
Style(rawValue: style) ?? .marker Style(rawValue: style) ?? .marker
} }
var batchIndex: Int = 0
var quadIndex: Int = -1 var quadIndex: Int = -1
var vertexIndex: Int = -1 var vertexIndex: Int = -1
var keyPoints: [CGPoint] = [] var keyPoints: [CGPoint] = []
@@ -58,22 +80,47 @@ final class Stroke: NSManagedObject {
} }
func loadVertices() { func loadVertices() {
vertices = quads guard let object else { return }
.flatMap { ($0 as? Quad)?.generateVertices(color) ?? [] } for quad in object.quads {
guard let quad = quad as? QuadObject else { continue }
vertices.append(contentsOf: Quad(object: quad).generateVertices(object.color))
}
vertexCount = vertices.endIndex vertexCount = vertices.endIndex
} }
func addQuad(at point: CGPoint, rotation: CGFloat, shape: QuadShape) -> Quad { func addQuad(at point: CGPoint, rotation: CGFloat, shape: QuadShape) -> Quad {
let quad = Quad(context: Persistence.context) let quad = Quad(
quad.id = UUID() origin: point,
quad.origin = point size: thickness,
quad.rotation = rotation rotation: rotation,
quad.size = thickness shape: shape.rawValue
quad.shape = shape.rawValue )
quads.add(quad) quads.append(quad)
quad.stroke = self
return quad return quad
} }
func removeQuads(from index: Int) {
let dropCount = quads.endIndex - max(1, index)
quads.removeLast(dropCount)
let quads = Array(quads[batchIndex..<index])
batchIndex = index
Persistence.backgroundContext.perform { [weak self, quads] in
self?.saveQuads(for: quads)
}
}
func saveQuads(for quads: [Quad]) {
for _quad in quads {
let quad = QuadObject(context: Persistence.backgroundContext)
quad.originX = _quad.originX
quad.originY = _quad.originY
quad.size = _quad.size
quad.rotation = _quad.rotation
quad.shape = _quad.shape
quad.stroke = object
object?.quads.add(quad)
}
}
} }
extension Stroke: Drawable { extension Stroke: Drawable {
+5 -3
View File
@@ -51,9 +51,11 @@ class History: ObservableObject {
redoCache = redoStack redoCache = redoStack
for event in redoStack { for event in redoStack {
switch event { switch event {
case .stroke(let stroke): case .stroke(let _stroke):
Persistence.performe { context in Persistence.backgroundContext.perform {
context.delete(stroke) if let stroke = _stroke.object {
Persistence.backgroundContext.delete(stroke)
}
} }
} }
} }
@@ -44,7 +44,8 @@ class CacheRenderPass: RenderPass {
descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load
descriptor.colorAttachments[0].storeAction = .store descriptor.colorAttachments[0].storeAction = .store
if let stroke = canvas.graphicContext.currentStroke { let graphicContext = canvas.graphicContext
if let stroke = graphicContext.currentStroke {
if stroke.isEraserPenStyle { if stroke.isEraserPenStyle {
eraserRenderPass.stroke = stroke eraserRenderPass.stroke = stroke
eraserRenderPass.descriptor = descriptor eraserRenderPass.descriptor = descriptor
@@ -36,17 +36,16 @@ class GraphicRenderPass: RenderPass {
guard let descriptor else { return } guard let descriptor else { return }
guard let graphicPipelineState else { return } guard let graphicPipelineState else { return }
let graphicContext = canvas.graphicContext
guard let graphicTexture else { return } guard let graphicTexture else { return }
descriptor.colorAttachments[0].texture = graphicTexture descriptor.colorAttachments[0].texture = graphicTexture
descriptor.colorAttachments[0].clearColor = MTLClearColor(red: 1, green: 1, blue: 1, alpha: 0) descriptor.colorAttachments[0].clearColor = MTLClearColor(red: 1, green: 1, blue: 1, alpha: 0)
descriptor.colorAttachments[0].storeAction = .store descriptor.colorAttachments[0].storeAction = .store
let graphicContext = canvas.graphicContext
if renderer.redrawsGraphicRender { if renderer.redrawsGraphicRender {
canvas.setGraphicRenderType(.finished) canvas.setGraphicRenderType(.finished)
for stroke in graphicContext.strokes.array { for stroke in graphicContext.strokes {
guard let stroke = stroke as? Stroke else { continue }
if graphicContext.previousStroke === stroke || graphicContext.currentStroke === stroke { if graphicContext.previousStroke === stroke || graphicContext.currentStroke === stroke {
continue continue
} }
@@ -62,9 +62,6 @@ class CanvasViewController: UIViewController {
override func viewDidDisappear(_ animated: Bool) { override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated) super.viewDidDisappear(animated)
history.resetRedo() history.resetRedo()
Persistence.performe { context in
context.refresh(canvas, mergeChanges: false)
}
} }
} }
@@ -32,7 +32,7 @@ class DrawingView: UIView {
} }
func updateDrawableSize(with size: CGSize) { func updateDrawableSize(with size: CGSize) {
renderView.drawableSize = size.multiply(by: 2.5) renderView.drawableSize = size.multiply(by: 2.0)
} }
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
-21
View File
@@ -1,21 +0,0 @@
//
// Memo.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import CoreData
import Foundation
@objc(Memo)
final class Memo: NSManagedObject {
@NSManaged var id: UUID
@NSManaged var title: String
@NSManaged var data: Data
@NSManaged var createdAt: Date
@NSManaged var updatedAt: Date
@NSManaged var canvas: Canvas
}
extension Memo: Identifiable { }
+8 -1
View File
@@ -6,15 +6,22 @@
// //
import SwiftUI import SwiftUI
import CoreData
struct MemoView: View { struct MemoView: View {
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
@Environment(\.managedObjectContext) var managedObjectContext @Environment(\.managedObjectContext) var managedObjectContext
@StateObject var tool = Tool() @StateObject var tool = Tool()
@StateObject var canvas: Canvas
@StateObject var history = History() @StateObject var history = History()
@EnvironmentObject var canvas: Canvas let memo: MemoObject
init(memo: MemoObject) {
self.memo = memo
self._canvas = StateObject(wrappedValue: Canvas(size: memo.canvas.size, canvasID: memo.canvas.objectID))
}
var body: some View { var body: some View {
CanvasView() CanvasView()
+22 -26
View File
@@ -10,9 +10,9 @@ import SwiftUI
struct MemosView: View { struct MemosView: View {
@Environment(\.managedObjectContext) var managedObjectContext @Environment(\.managedObjectContext) var managedObjectContext
@FetchRequest(sortDescriptors: []) var memos: FetchedResults<Memo> @FetchRequest(sortDescriptors: []) var memoObjects: FetchedResults<MemoObject>
@State var memo: Memo? @State var memo: MemoObject?
var body: some View { var body: some View {
NavigationStack { NavigationStack {
@@ -30,15 +30,14 @@ struct MemosView: View {
} }
} }
.fullScreenCover(item: $memo) { memo in .fullScreenCover(item: $memo) { memo in
MemoView() MemoView(memo: memo)
.environmentObject(memo.canvas)
} }
} }
var memoGrid: some View { var memoGrid: some View {
ScrollView { ScrollView {
LazyVGrid(columns: .init(repeating: GridItem(.flexible()), count: 3)) { LazyVGrid(columns: .init(repeating: GridItem(.flexible()), count: 3)) {
ForEach(memos) { memo in ForEach(memoObjects) { memo in
memoCard(memo) memoCard(memo)
} }
} }
@@ -46,47 +45,44 @@ struct MemosView: View {
} }
} }
func memoCard(_ memo: Memo) -> some View { func memoCard(_ memoObject: MemoObject) -> some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Rectangle() Rectangle()
.frame(height: 150) .frame(height: 150)
Text(memo.title) Text(memoObject.title)
} }
.onTapGesture { .onTapGesture {
openMemo(for: memo) openMemo(for: memoObject)
} }
} }
func createMemo(title: String) { func createMemo(title: String) {
do { do {
let memo = Memo(context: managedObjectContext) let memoObject = MemoObject(context: managedObjectContext)
memo.id = UUID() memoObject.title = title
memo.title = title memoObject.createdAt = .now
memo.createdAt = .now memoObject.updatedAt = .now
memo.updatedAt = .now
let canvas = Canvas(context: managedObjectContext) let canvasObject = CanvasObject(context: managedObjectContext)
canvas.id = UUID() canvasObject.width = 4_000
canvas.width = 4_000 canvasObject.height = 4_000
canvas.height = 4_000
let graphicContext = GraphicContext(context: managedObjectContext) let graphicContextObject = GraphicContextObject(context: managedObjectContext)
graphicContext.id = UUID() graphicContextObject.strokes = []
graphicContext.strokes = []
memo.canvas = canvas memoObject.canvas = canvasObject
canvas.memo = memo canvasObject.memo = memoObject
canvas.graphicContext = graphicContext canvasObject.graphicContext = graphicContextObject
graphicContext.canvas = canvas graphicContextObject.canvas = canvasObject
try managedObjectContext.save() try managedObjectContext.save()
openMemo(for: memo) openMemo(for: memoObject)
} catch { } catch {
NSLog("[Memola] - \(error.localizedDescription)") NSLog("[Memola] - \(error.localizedDescription)")
} }
} }
func openMemo(for memo: Memo) { func openMemo(for memo: MemoObject) {
self.memo = memo self.memo = memo
} }
} }
+23 -1
View File
@@ -21,7 +21,9 @@ class Persistence {
static var backgroundContext: NSManagedObjectContext = { static var backgroundContext: NSManagedObjectContext = {
let context = shared.persistentContainer.newBackgroundContext() let context = shared.persistentContainer.newBackgroundContext()
context.automaticallyMergesChangesFromParent = true context.undoManager = nil
// context.automaticallyMergesC hangesFromParent = true
return context return context
}() }()
@@ -87,4 +89,24 @@ class Persistence {
} }
} }
} }
static func saveIfNeededInBackground() {
if backgroundContext.hasChanges {
do {
try backgroundContext.save()
} catch {
NSLog("[Memola] - \(error.localizedDescription)")
}
}
}
}
extension Persistence {
static func background(_ task: @escaping (NSManagedObjectContext) async throws -> Void, errorHandler: ((Error) async -> Void)? = nil) async {
do {
try await task(backgroundContext)
} catch {
await errorHandler?(error)
}
}
} }
@@ -0,0 +1,22 @@
//
// CanvasObject.swift
// Memola
//
// Created by Dscyre Scotti on 5/11/24.
//
import CoreData
import Foundation
@objc(CanvasObject)
final class CanvasObject: NSManagedObject {
@NSManaged var width: CGFloat
@NSManaged var height: CGFloat
@NSManaged var memo: MemoObject?
@NSManaged var graphicContext: GraphicContextObject
var size: CGSize {
CGSize(width: width, height: height)
}
}
@@ -0,0 +1,15 @@
//
// GraphicContextObject.swift
// Memola
//
// Created by Dscyre Scotti on 5/11/24.
//
import CoreData
import Foundation
@objc(GraphicContextObject)
final class GraphicContextObject: NSManagedObject {
@NSManaged var canvas: CanvasObject?
@NSManaged var strokes: NSMutableOrderedSet
}
@@ -0,0 +1,18 @@
//
// MemoObject.swift
// Memola
//
// Created by Dscyre Scotti on 5/11/24.
//
import CoreData
import Foundation
@objc(MemoObject)
final class MemoObject: NSManagedObject, Identifiable {
@NSManaged var title: String
@NSManaged var data: Data
@NSManaged var createdAt: Date
@NSManaged var updatedAt: Date
@NSManaged var canvas: CanvasObject
}
@@ -0,0 +1,19 @@
//
// QuadObject.swift
// Memola
//
// Created by Dscyre Scotti on 5/11/24.
//
import CoreData
import Foundation
@objc(QuadObject)
final class QuadObject: NSManagedObject {
@NSManaged var originX: CGFloat
@NSManaged var originY: CGFloat
@NSManaged var size: CGFloat
@NSManaged var rotation: CGFloat
@NSManaged var shape: Int16
@NSManaged var stroke: StrokeObject?
}
@@ -0,0 +1,19 @@
//
// StrokeObject.swift
// Memola
//
// Created by Dscyre Scotti on 5/11/24.
//
import CoreData
import Foundation
@objc(StrokeObject)
final class StrokeObject: NSManagedObject {
@NSManaged var color: [CGFloat]
@NSManaged var style: Int16
@NSManaged var createdAt: Date
@NSManaged var thickness: CGFloat
@NSManaged var quads: NSMutableOrderedSet
@NSManaged var graphicContext: GraphicContextObject?
}
@@ -1,40 +1,35 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22757" systemVersion="23B74" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier=""> <model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22757" systemVersion="23B74" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Canvas" representedClassName="Canvas" syncable="YES"> <entity name="CanvasObject" representedClassName="CanvasObject" syncable="YES">
<attribute name="height" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES" customClassName="CGFloat"/> <attribute name="height" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES" customClassName="CGFloat"/>
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="width" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES" customClassName="CGFloat"/> <attribute name="width" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES" customClassName="CGFloat"/>
<relationship name="graphicContext" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="GraphicContext" inverseName="canvas" inverseEntity="GraphicContext"/> <relationship name="graphicContext" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="GraphicContextObject" inverseName="canvas" inverseEntity="GraphicContextObject"/>
<relationship name="memo" maxCount="1" deletionRule="Deny" destinationEntity="Memo" inverseName="canvas" inverseEntity="Memo"/> <relationship name="memo" maxCount="1" deletionRule="Deny" destinationEntity="MemoObject" inverseName="canvas" inverseEntity="MemoObject"/>
</entity> </entity>
<entity name="GraphicContext" representedClassName="GraphicContext" syncable="YES"> <entity name="GraphicContextObject" representedClassName="GraphicContextObject" syncable="YES">
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/> <relationship name="canvas" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="CanvasObject" inverseName="graphicContext" inverseEntity="CanvasObject"/>
<relationship name="canvas" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Canvas" inverseName="graphicContext" inverseEntity="Canvas"/> <relationship name="strokes" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="StrokeObject" inverseName="graphicContext" inverseEntity="StrokeObject"/>
<relationship name="strokes" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="Stroke" inverseName="graphicContext" inverseEntity="Stroke"/>
</entity> </entity>
<entity name="Memo" representedClassName="Memo" syncable="YES"> <entity name="MemoObject" representedClassName="MemoObject" syncable="YES">
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/> <attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="title" attributeType="String"/> <attribute name="title" attributeType="String"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/> <attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="canvas" maxCount="1" deletionRule="Cascade" destinationEntity="Canvas" inverseName="memo" inverseEntity="Canvas"/> <relationship name="canvas" maxCount="1" deletionRule="Cascade" destinationEntity="CanvasObject" inverseName="memo" inverseEntity="CanvasObject"/>
</entity> </entity>
<entity name="Quad" representedClassName="Quad" syncable="YES"> <entity name="QuadObject" representedClassName="QuadObject" syncable="YES">
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="originX" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/> <attribute name="originX" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="originY" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/> <attribute name="originY" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="rotation" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/> <attribute name="rotation" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="shape" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/> <attribute name="shape" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="size" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/> <attribute name="size" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<relationship name="stroke" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Stroke" inverseName="quads" inverseEntity="Stroke"/> <relationship name="stroke" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StrokeObject" inverseName="quads" inverseEntity="StrokeObject"/>
</entity> </entity>
<entity name="Stroke" representedClassName="Stroke" syncable="YES"> <entity name="StrokeObject" representedClassName="StrokeObject" syncable="YES">
<attribute name="color" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/> <attribute name="color" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/> <attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="style" 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"/> <attribute name="thickness" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<relationship name="graphicContext" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="GraphicContext" inverseName="strokes" inverseEntity="GraphicContext"/> <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="Quad" inverseName="stroke" inverseEntity="Quad"/> <relationship name="quads" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="QuadObject" inverseName="stroke" inverseEntity="QuadObject"/>
</entity> </entity>
</model> </model>