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

View File

@@ -64,9 +64,13 @@
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 */; };
ECA739052BE61E3100A4542E /* Memo.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA739042BE61E3100A4542E /* Memo.swift */; };
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 */; };
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 */
/* Begin PBXFileReference section */
@@ -128,9 +132,13 @@
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>"; };
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>"; };
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 */
/* Begin PBXFrameworksBuildPhase section */
@@ -226,7 +234,6 @@
ECA7387B2BE5EF3500A4542E /* Memo */ = {
isa = PBXGroup;
children = (
ECA739042BE61E3100A4542E /* Memo.swift */,
ECA7387C2BE5EF4B00A4542E /* MemoView.swift */,
ECA739072BE623F300A4542E /* PenToolView.swift */,
);
@@ -453,6 +460,7 @@
ECA738FA2BE61B1700A4542E /* Persistence */ = {
isa = PBXGroup;
children = (
ECFA151E2BEF21BE00455818 /* Objects */,
ECA739022BE61DE700A4542E /* Core */,
);
path = Persistence;
@@ -491,6 +499,18 @@
path = Core;
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 */
/* Begin PBXNativeTarget section */
@@ -569,14 +589,17 @@
ECA7389C2BE601AF00A4542E /* GridVertex.swift in Sources */,
ECA738A82BE6025900A4542E /* GraphicUniforms.swift in Sources */,
ECA738E62BE611FD00A4542E /* CGRect++.swift in Sources */,
ECFA15262BEF224900455818 /* StrokeObject.swift in Sources */,
ECA738E82BE6120F00A4542E /* Color++.swift in Sources */,
ECA738FC2BE61C5200A4542E /* Persistence.swift in Sources */,
ECA7387A2BE5EF0400A4542E /* MemosView.swift in Sources */,
ECA738BA2BE60DEF00A4542E /* HistoryAction.swift in Sources */,
ECFA15222BEF21F500455818 /* CanvasObject.swift in Sources */,
ECA738AA2BE6026D00A4542E /* Uniforms.swift in Sources */,
ECA7387D2BE5EF4B00A4542E /* MemoView.swift in Sources */,
ECA738DA2BE60FF100A4542E /* CacheRenderPass.swift in Sources */,
ECA738CD2BE60F2F00A4542E /* GridContext.swift in Sources */,
ECFA15202BEF21EF00455818 /* MemoObject.swift in Sources */,
ECA738C62BE60E9D00A4542E /* EraserPenStyle.swift in Sources */,
ECA738EA2BE6122E00A4542E /* CGPoint++.swift in Sources */,
ECA738A62BE6023F00A4542E /* GridUniforms.swift in Sources */,
@@ -594,11 +617,11 @@
ECA738952BE6012D00A4542E /* ViewPort.metal in Sources */,
ECA739012BE61D9C00A4542E /* MemolaModel.xcdatamodeld in Sources */,
ECA738F02BE6127700A4542E /* CGSize++.swift in Sources */,
ECFA15242BEF223300455818 /* GraphicContextObject.swift in Sources */,
ECA738EC2BE6124E00A4542E /* CGAffineTransform++.swift in Sources */,
ECA738E22BE610D000A4542E /* GraphicRenderPass.swift in Sources */,
ECA738DC2BE6108D00A4542E /* StrokeRenderPass.swift in Sources */,
ECA738F42BE612A000A4542E /* Array++.swift in Sources */,
ECA739052BE61E3100A4542E /* Memo.swift in Sources */,
EC4538892BEBCAE000A86FEC /* Quad.swift in Sources */,
ECA7388F2BE600DA00A4542E /* Grid.metal in Sources */,
ECA738C92BE60EF700A4542E /* GraphicContext.swift in Sources */,
@@ -607,6 +630,7 @@
ECA738B32BE60D9E00A4542E /* CanvasView.swift in Sources */,
ECA738C42BE60E8800A4542E /* MarkerPenStyle.swift in Sources */,
ECA738BF2BE60E3400A4542E /* Pen.swift in Sources */,
ECFA15282BEF225000455818 /* QuadObject.swift in Sources */,
ECA738932BE6011100A4542E /* Stroke.metal in Sources */,
ECA738B62BE60DCD00A4542E /* History.swift in Sources */,
ECA738D22BE60F7B00A4542E /* Stroke.swift in Sources */,

View File

@@ -10,11 +10,9 @@ import MetalKit
import CoreData
import Foundation
@objc(GraphicContext)
final class GraphicContext: NSManagedObject {
@NSManaged var id: UUID
@NSManaged var canvas: Canvas?
@NSManaged var strokes: NSMutableOrderedSet
final class GraphicContext: @unchecked Sendable {
var strokes: [Stroke] = []
var object: GraphicContextObject?
var currentStroke: Stroke?
var previousStroke: Stroke?
@@ -24,8 +22,7 @@ final class GraphicContext: NSManagedObject {
var vertexCount: Int = 4
var vertexBuffer: MTLBuffer?
override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
super.init(entity: entity, insertInto: context)
init() {
setViewPortVertices()
}
@@ -40,21 +37,37 @@ final class GraphicContext: NSManagedObject {
}
func undoGraphic() {
guard let stroke = strokes.lastObject as? Stroke else { return }
strokes.remove(stroke)
stroke.graphicContext = nil
guard !strokes.isEmpty else { return }
let stroke = strokes.removeLast()
Persistence.backgroundContext.perform {
stroke.object?.graphicContext = nil
Persistence.saveIfNeededInBackground()
}
previousStroke = nil
Persistence.saveIfNeeded()
}
func redoGraphic(for event: HistoryEvent) {
switch event {
case .stroke(let stroke):
strokes.add(stroke)
stroke.graphicContext = self
strokes.append(stroke)
Persistence.backgroundContext.perform { [weak self] in
stroke.object?.graphicContext = self?.object
Persistence.saveIfNeededInBackground()
}
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 {
func beginStroke(at point: CGPoint, pen: Pen) -> Stroke {
let stroke = Stroke(context: Persistence.context)
stroke.id = UUID()
stroke.color = pen.color
stroke.style = pen.strokeStyle.rawValue
stroke.thickness = pen.thickness
stroke.createdAt = .now
stroke.quads = []
stroke.graphicContext = self
strokes.add(stroke)
let stroke = Stroke(
color: pen.color,
style: pen.strokeStyle.rawValue,
createdAt: .now,
thickness: pen.thickness
)
Persistence.backgroundContext.perform { [graphicContext = object, _stroke = stroke] in
let stroke = StrokeObject(context: Persistence.backgroundContext)
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
currentPoint = point
currentStroke?.begin(at: point)
@@ -101,19 +123,31 @@ extension GraphicContext {
func endStroke(at point: CGPoint) {
guard currentPoint != nil, let currentStroke else { return }
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
self.currentStroke = nil
self.currentPoint = nil
}
func cancelStroke() {
if let stroke = strokes.lastObject as? Stroke {
Persistence.performe { context in
strokes.remove(stroke)
context.delete(stroke)
if !strokes.isEmpty {
let stroke = strokes.removeLast()
Persistence.backgroundContext.perform { [graphicContext = object, _stroke = stroke] in
if let stroke = _stroke.object {
graphicContext?.strokes.remove(stroke)
Persistence.backgroundContext.delete(stroke)
}
Persistence.saveIfNeededInBackground()
}
Persistence.saveIfNeeded()
}
currentStroke = nil
currentPoint = nil

View File

@@ -10,16 +10,14 @@ import CoreData
import MetalKit
import Foundation
@objc(Canvas)
final class Canvas: NSManagedObject, Identifiable {
@NSManaged var id: UUID
@NSManaged var width: CGFloat
@NSManaged var height: CGFloat
@NSManaged var memo: Memo?
@NSManaged var graphicContext: GraphicContext
final class Canvas: ObservableObject, Identifiable, @unchecked Sendable {
let size: CGSize
let canvasID: NSManagedObjectID
let gridContext = GridContext()
var graphicContext = GraphicContext()
let viewPortContext = ViewPortContext()
let maximumZoomScale: CGFloat = 28
let minimumZoomScale: CGFloat = 3.1
@@ -28,9 +26,13 @@ final class Canvas: NSManagedObject, Identifiable {
var zoomScale: CGFloat = .zero
var uniformsBuffer: MTLBuffer?
init(size: CGSize, canvasID: NSManagedObjectID) {
self.size = size
self.canvasID = canvasID
}
@Published var state: State = .initial
var size: CGSize { CGSize(width: width, height: height) }
var hasValidStroke: Bool {
if let currentStroke = graphicContext.currentStroke {
return Date.now.timeIntervalSince(currentStroke.createdAt) * 1000 > 80
@@ -42,16 +44,21 @@ final class Canvas: NSManagedObject, Identifiable {
// MARK: - Actions
extension Canvas {
func load() {
state = .loading
let start = Date().formatted(.dateTime.minute().second().secondFraction(.fractional(5)))
for stroke in graphicContext.strokes {
if let stroke = stroke as? Stroke {
stroke.loadVertices()
Persistence.backgroundContext.perform { [weak self, canvasID] in
DispatchQueue.main.async { [weak self] in
self?.state = .loading
}
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? {
graphicContext.strokes.lastObject as? Stroke
graphicContext.strokes.last
}
}

View File

@@ -8,15 +8,28 @@
import CoreData
import Foundation
@objc(Quad)
class Quad: NSManagedObject {
@NSManaged var id: UUID
@NSManaged var originX: CGFloat
@NSManaged var originY: CGFloat
@NSManaged var size: CGFloat
@NSManaged var rotation: CGFloat
@NSManaged var shape: Int16
@NSManaged var stroke: Stroke?
struct Quad {
var originX: CGFloat
var originY: CGFloat
var size: CGFloat
var rotation: CGFloat
var shape: Int16
init(object: QuadObject) {
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 {
get { CGPoint(x: originX, y: originY) }

View File

@@ -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) {
if index < 0 {
stroke.vertices.removeAll()
discardQuads(from: quadIndex + 1, on: stroke)
} else {
let count = stroke.vertices.endIndex
let dropCount = count - (max(0, index) + 1)
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)
}
}

View File

@@ -9,15 +9,36 @@ import MetalKit
import CoreData
import Foundation
@objc(Stroke)
final class Stroke: NSManagedObject {
@NSManaged var id: UUID
@NSManaged var color: [CGFloat]
@NSManaged var style: Int16
@NSManaged var createdAt: Date
@NSManaged var thickness: CGFloat
@NSManaged var quads: NSMutableOrderedSet
@NSManaged var graphicContext: GraphicContext?
final class Stroke: @unchecked Sendable {
var object: StrokeObject?
var color: [CGFloat]
var style: Int16
var createdAt: Date
var thickness: CGFloat
var quads: [Quad]
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
@@ -25,6 +46,7 @@ final class Stroke: NSManagedObject {
Style(rawValue: style) ?? .marker
}
var batchIndex: Int = 0
var quadIndex: Int = -1
var vertexIndex: Int = -1
var keyPoints: [CGPoint] = []
@@ -58,22 +80,47 @@ final class Stroke: NSManagedObject {
}
func loadVertices() {
vertices = quads
.flatMap { ($0 as? Quad)?.generateVertices(color) ?? [] }
guard let object else { return }
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
}
func addQuad(at point: CGPoint, rotation: CGFloat, shape: QuadShape) -> Quad {
let quad = Quad(context: Persistence.context)
quad.id = UUID()
quad.origin = point
quad.rotation = rotation
quad.size = thickness
quad.shape = shape.rawValue
quads.add(quad)
quad.stroke = self
let quad = Quad(
origin: point,
size: thickness,
rotation: rotation,
shape: shape.rawValue
)
quads.append(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 {

View File

@@ -51,9 +51,11 @@ class History: ObservableObject {
redoCache = redoStack
for event in redoStack {
switch event {
case .stroke(let stroke):
Persistence.performe { context in
context.delete(stroke)
case .stroke(let _stroke):
Persistence.backgroundContext.perform {
if let stroke = _stroke.object {
Persistence.backgroundContext.delete(stroke)
}
}
}
}

View File

@@ -44,7 +44,8 @@ class CacheRenderPass: RenderPass {
descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load
descriptor.colorAttachments[0].storeAction = .store
if let stroke = canvas.graphicContext.currentStroke {
let graphicContext = canvas.graphicContext
if let stroke = graphicContext.currentStroke {
if stroke.isEraserPenStyle {
eraserRenderPass.stroke = stroke
eraserRenderPass.descriptor = descriptor

View File

@@ -36,17 +36,16 @@ class GraphicRenderPass: RenderPass {
guard let descriptor else { return }
guard let graphicPipelineState else { return }
let graphicContext = canvas.graphicContext
guard let graphicTexture else { return }
descriptor.colorAttachments[0].texture = graphicTexture
descriptor.colorAttachments[0].clearColor = MTLClearColor(red: 1, green: 1, blue: 1, alpha: 0)
descriptor.colorAttachments[0].storeAction = .store
let graphicContext = canvas.graphicContext
if renderer.redrawsGraphicRender {
canvas.setGraphicRenderType(.finished)
for stroke in graphicContext.strokes.array {
guard let stroke = stroke as? Stroke else { continue }
for stroke in graphicContext.strokes {
if graphicContext.previousStroke === stroke || graphicContext.currentStroke === stroke {
continue
}

View File

@@ -62,9 +62,6 @@ class CanvasViewController: UIViewController {
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
history.resetRedo()
Persistence.performe { context in
context.refresh(canvas, mergeChanges: false)
}
}
}

View File

@@ -32,7 +32,7 @@ class DrawingView: UIView {
}
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 {

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 { }

View File

@@ -6,15 +6,22 @@
//
import SwiftUI
import CoreData
struct MemoView: View {
@Environment(\.dismiss) var dismiss
@Environment(\.managedObjectContext) var managedObjectContext
@StateObject var tool = Tool()
@StateObject var canvas: Canvas
@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 {
CanvasView()

View File

@@ -10,9 +10,9 @@ import SwiftUI
struct MemosView: View {
@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 {
NavigationStack {
@@ -30,15 +30,14 @@ struct MemosView: View {
}
}
.fullScreenCover(item: $memo) { memo in
MemoView()
.environmentObject(memo.canvas)
MemoView(memo: memo)
}
}
var memoGrid: some View {
ScrollView {
LazyVGrid(columns: .init(repeating: GridItem(.flexible()), count: 3)) {
ForEach(memos) { memo in
ForEach(memoObjects) { memo in
memoCard(memo)
}
}
@@ -46,47 +45,44 @@ struct MemosView: View {
}
}
func memoCard(_ memo: Memo) -> some View {
func memoCard(_ memoObject: MemoObject) -> some View {
VStack(alignment: .leading) {
Rectangle()
.frame(height: 150)
Text(memo.title)
Text(memoObject.title)
}
.onTapGesture {
openMemo(for: memo)
openMemo(for: memoObject)
}
}
func createMemo(title: String) {
do {
let memo = Memo(context: managedObjectContext)
memo.id = UUID()
memo.title = title
memo.createdAt = .now
memo.updatedAt = .now
let memoObject = MemoObject(context: managedObjectContext)
memoObject.title = title
memoObject.createdAt = .now
memoObject.updatedAt = .now
let canvas = Canvas(context: managedObjectContext)
canvas.id = UUID()
canvas.width = 4_000
canvas.height = 4_000
let canvasObject = CanvasObject(context: managedObjectContext)
canvasObject.width = 4_000
canvasObject.height = 4_000
let graphicContext = GraphicContext(context: managedObjectContext)
graphicContext.id = UUID()
graphicContext.strokes = []
let graphicContextObject = GraphicContextObject(context: managedObjectContext)
graphicContextObject.strokes = []
memo.canvas = canvas
canvas.memo = memo
canvas.graphicContext = graphicContext
graphicContext.canvas = canvas
memoObject.canvas = canvasObject
canvasObject.memo = memoObject
canvasObject.graphicContext = graphicContextObject
graphicContextObject.canvas = canvasObject
try managedObjectContext.save()
openMemo(for: memo)
openMemo(for: memoObject)
} catch {
NSLog("[Memola] - \(error.localizedDescription)")
}
}
func openMemo(for memo: Memo) {
func openMemo(for memo: MemoObject) {
self.memo = memo
}
}

View File

@@ -21,7 +21,9 @@ class Persistence {
static var backgroundContext: NSManagedObjectContext = {
let context = shared.persistentContainer.newBackgroundContext()
context.automaticallyMergesChangesFromParent = true
context.undoManager = nil
// context.automaticallyMergesC hangesFromParent = true
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)
}
}
}

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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?
}

View File

@@ -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?
}

View File

@@ -1,40 +1,35 @@
<?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="">
<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="id" attributeType="UUID" usesScalarValueType="NO"/>
<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="memo" maxCount="1" deletionRule="Deny" destinationEntity="Memo" inverseName="canvas" inverseEntity="Memo"/>
<relationship name="graphicContext" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="GraphicContextObject" inverseName="canvas" inverseEntity="GraphicContextObject"/>
<relationship name="memo" maxCount="1" deletionRule="Deny" destinationEntity="MemoObject" inverseName="canvas" inverseEntity="MemoObject"/>
</entity>
<entity name="GraphicContext" representedClassName="GraphicContext" syncable="YES">
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
<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="Stroke" inverseName="graphicContext" inverseEntity="Stroke"/>
<entity name="GraphicContextObject" representedClassName="GraphicContextObject" syncable="YES">
<relationship name="canvas" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="CanvasObject" inverseName="graphicContext" inverseEntity="CanvasObject"/>
<relationship name="strokes" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="StrokeObject" inverseName="graphicContext" inverseEntity="StrokeObject"/>
</entity>
<entity name="Memo" representedClassName="Memo" syncable="YES">
<entity name="MemoObject" representedClassName="MemoObject" syncable="YES">
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="title" attributeType="String"/>
<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 name="Quad" representedClassName="Quad" syncable="YES">
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
<entity name="QuadObject" representedClassName="QuadObject" syncable="YES">
<attribute name="originX" 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="shape" attributeType="Integer 16" defaultValueString="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 name="Stroke" representedClassName="Stroke" syncable="YES">
<entity name="StrokeObject" representedClassName="StrokeObject" syncable="YES">
<attribute name="color" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/>
<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="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="quads" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="Quad" inverseName="stroke" inverseEntity="Quad"/>
<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>
</model>