SwiftyDraw
SwiftyDraw copied to clipboard
How can I save and restore drawings from the canvas?
How can I save the drawing to a database and later on give the possibility to edit or add new content to the same drawing?
see #18
issue #18 help in save as image. But I want to restore the same drawing and user can select the drawing and remove it like Google keep.
Ohh I get it. Sorry, I misread your question.
This is an issue I've been working on for a while. Doing it the image way is simple, but is not really a great solution because you can't edit it anymore. I've tried Pathology, and it works well for small drawings. However, once you have more lines, it takes forever to read & transform them from a json string.
Thank you for sharing.
Let me know if you find a solution that we could use for the library. Would be a great addition :)
I've been able to accomplish this by storing the lines
property, and then later passing them to display(lines: [Line])
What exactly did you manage to store? Using Pathology? How does it perform with a lot of lines?
I build my own simple drawing app which temporary stores the lines as structs (similar to your approach). I made those line struct codable therefore I can export the array of line structs as JSON. I retrieve the drawing by decoding the JSON to the array of line structs and updating the view. I tested a drawing with roughly 2000 CGPoints and it took less than a second to display the drawing in the UIView. Let me know if you have any questions!
@lucashoeft sounds great! Would you like to share your implementation? Did you use SwiftyDraw for your app or some other library/custom solution?
Yeah, sure. I followed the tutorial on YT: https://www.youtube.com/watch?v=E2NTCmEsdSE (it's a three part series) so I did not use SwiftyDraw, but complexity should be similar
The structs (simplified):
struct Canvas: Codable {
var lines: [Line]
}
struct Line: Codable {
var points: [CGPoint]
var color: String
var strokeWidth: Float
var strokeOpacity: Float
var lineStyle: String
}
The User Input in the UIView
var canvas = Canvas()
override func draw(_ rect: CGRect) {
super.draw(rect)
guard let context = UIGraphicsGetCurrentContext() else { return }
canvas.lines?.forEach { (line) in
context.setStrokeColor(UIColor.init(hex: line.color)?.cgColor ?? UIColor.black.cgColor)
context.setLineWidth(line.strokeWidth)
context.setLineCap(.round)
context.setLineJoin(.round)
for (i, p) in line.points.enumerated() {
if i == 0 {
context.move(to: p)
} else {
context.addLine(to: p)
}
}
context.strokePath()
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
canvas.lines?.append(Line(points: [], color: strokeColor, strokeWidth: strokeWidth, strokeOpacity: strokeOpacity, lineStyle: "Line"))
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let point = touches.first?.location(in: self) else { return }
guard var lastLine = canvas.lines?.popLast() else { return }
lastLine.points.append(point)
canvas.lines?.append(lastLine)
setNeedsDisplay()
}
The export functions (the conversion to string is optional as I wanted to use strings for testing saving/retrieving locally):
func getDrawing() -> String {
let jsonEncoder = JSONEncoder()
let jsonData = try! jsonEncoder.encode(canvas)
if let jsonString = String(data: jsonData, encoding: String.Encoding.utf8) {
return jsonString
} else {
return ""
}
}
func setDrawing(jsonString: String) {
let jsonDecoder = JSONDecoder()
let retrievedCanvas = try! jsonDecoder.decode(Canvas.self, from: jsonString.data(using: .utf8)!)
canvas = retrievedCanvas
setNeedsDisplay()
}
@lucashoeft thanks for sharing! the implementation of the drawing logic looks pretty similar as far as I can tell. I will try to integrate this into SwiftyDraw and see if I can get it to work. Will keep you posted :)
Yikes. Forgot to follow up on this. Sorry guys. Looks like @lucashoeft has the right idea.
@evancooper9 you can still share what you've done if you'd like. We can compare the approaches and see which one works best for SwiftyDraw :)
@lucashoeft I've checked out your code above. Regarding the drawing itself: when drawing really fast, the lines get very choppy. Any idea how to deal with that?
It's because of the "simple" drawing function (it only connects the detected touches/points with direct lines..
for (i, p) in line.points.enumerated() {
if i == 0 {
context.move(to: p)
} else {
context.addLine(to: p)
}
}
context.strokePath()
In SwiftyDraw, MidPoints get calculated and added to the array of points to have more entries to display a smoother line. I'm not sure if it is Hermite Interpolation
but the approach looks quite similar
@lucashoeft I figured that, but how do we combine the two approaches? The thing is that CGMutablePath
(SwiftyDraw uses) isn't mutable - with Pathology it is, but then we're back at the major performance issues with decoding the CGMutablePath
s
I have same issue. I solve it by converting CGmutablePath to UIBezierPath to store in Coredata.then convert again UIbaizerPath to CGmutablePath to display on SwiftyDrawView.
Following is Code to Store CGmutablePath in Coredata :
for drawpath in drawbleView.drawingHistory {
let baizerpath = UIBezierPath(cgPath: drawpath.path)
let drawableBrush = drawpath.brush
let brushobj = Brushh(context: context)
brushobj.color = drawableBrush.color.toHexString()
brushobj.width = Float(drawableBrush.width)
if drawableBrush.blendMode == .clear {
brushobj.mode = 0
}
else {
brushobj.mode = 1
}
let linesobj = Lines(context: context)
linesobj.path = baizerpath as NSObject
linesobj.brush = brushobj
arrayofLines.append(linesobj)
}
Following is code to Create CGmutablePath From UIbaizerPath
let element = baizaar.cgPath.pathElements()
let mutablePath = CGMutablePath()
for pathElement in element {
switch pathElement {
case .moveToPoint(let pt): mutablePath.move(to: pt)
case .addLineToPoint(let pt): mutablePath.addLine(to: pt)
case .addQuadCurveToPoint(let pt1, let pt2): mutablePath.addQuadCurve(to: pt2, control: pt1)
case .addCurveToPoint(let pt1, let pt2, let pt3): print("do nothing \(pt1) \(pt2) \(pt3)")
case .closeSubpath: mutablePath.closeSubpath()
}
}
let drawableBrush = SwiftyDrawView.Line(path: mutablePath, brush: brush)
following is extension to get Pathelement From UIbaizerPath
public enum PathElement {
case moveToPoint(CGPoint)
case addLineToPoint(CGPoint)
case addQuadCurveToPoint(CGPoint, CGPoint)
case addCurveToPoint(CGPoint, CGPoint, CGPoint)
case closeSubpath
}
internal class Info {
var pathElements = [PathElement]()
}
//
// CGPathRef
//
public extension CGPath {
func pathElements() -> [PathElement] {
var info = Info()
self.apply(info: &info) { (info, element) -> Void in
if let infoPointer = UnsafeMutablePointer<Info>(OpaquePointer(info)) {
switch element.pointee.type {
case .moveToPoint:
let pt = element.pointee.points[0]
infoPointer.pointee.pathElements.append(PathElement.moveToPoint(pt))
//print("MoveToPoint \(pt)")
case .addLineToPoint:
let pt = element.pointee.points[0]
infoPointer.pointee.pathElements.append(PathElement.addLineToPoint(pt))
//print("AddLineToPoint \(pt)")
case .addQuadCurveToPoint:
let pt1 = element.pointee.points[0]
let pt2 = element.pointee.points[1]
infoPointer.pointee.pathElements.append(PathElement.addQuadCurveToPoint(pt1, pt2))
//print("AddQuadCurveToPoint \(pt1) \(pt2)")
case .addCurveToPoint:
let pt1 = element.pointee.points[0]
let pt2 = element.pointee.points[1]
let pt3 = element.pointee.points[2]
infoPointer.pointee.pathElements.append(PathElement.addCurveToPoint(pt1, pt2, pt3))
//print("AddCurveToPoint \(pt1) \(pt2) \(pt3)")
case .closeSubpath:
infoPointer.pointee.pathElements.append(PathElement.closeSubpath)
//print("CloseSubpath")
@unknown default:
print("do nothinh")
}
}
}
return info.pathElements
}
}
//
// operator ==
//
public func == (lhs: PathElement, rhs: PathElement) -> Bool {
switch (lhs, rhs) {
case (.moveToPoint(let a), .moveToPoint(let b)):
return a == b
case (.addLineToPoint(let a), .addLineToPoint(let b)):
return a == b
case (.addQuadCurveToPoint(let a1, let a2), .addQuadCurveToPoint(let b1, let b2)):
return a1.equalTo(b1) && a2.equalTo(b2)
case (.addCurveToPoint(let a1, let a2, let a3), .addCurveToPoint(let b1, let b2, let b3)):
return a1 == b1 && a2 == b2 && a3 == b3
case (.closeSubpath, .closeSubpath):
return true
default:
return false
}
}
Cool, thanks @bhaveshbc for sharing. Does your code:
- load & save data quickly?
- allow for smooth drawing? – as opposed to the previous approach with the choppy lines
- Yes it just take couple of second.
- Yes it preserve the quality.
Cool, then I'll try it out and merge into the repo if suitable.
So I just tried to implement your code and I have a couple of questions:
- in the first snippet, where do you get
context
from? - did you create a custom
init(context:)
method forBrush
andLine
? - in the end, which data do you save to core data?
arrayoflines
? - in the second snippet, where do you get
baizaar
from?
Would appreciate your help on this! @bhaveshbc
HI @LinusGeffarth sorry for late response.
- context is coredata ViewContext.ex: let context = appdel.persistentContainer.viewContext
- no those method created automatically by coredata according to model
- yes
- baizaar is baizaarpath
https://www.dropbox.com/s/adswe9z4cl6cbq3/DemoMLView%2019.zip?dl=0
@bhaveshbc so far I've not been able to get your code running. Would you mind forking the project and creating a PR? That'd be really helpful!
@kwccheng hey, see my comment from above about why that does not work as one would expect...
swiping between PDF images
What do you mean by that? I haven't seen any performance issues saving images...
I've managed to convert CGMutablePath to Data (and base64).
// encoding
let cgMutablePath = line.path
let uiBazierPath = UIBezierPath(cgPath: cgMutablePath)
let data = try? NSKeyedArchiver.archivedData(withRootObject: uiBaizerPath, requiringSecureCoding: false)
let base64String = data?.base64EncodedString()
// decoding
let data = Data(base64Encoded: base64String)
let uiBaizerPath = try! NSKeyedUnarchiver.unarchivedObject(ofClass: UIBezierPath.self, from: data)
let cgMutablePath = uiBaizerPath.cgPath as! CGMutablePath
I used this code to send/receive SwiftyDraw Lines using Firebase Realtime Database. It might be useful for this issue.
@blu3mo thanks for you contribution. How fast is the de-/encoding though? Can you load very many lines at a high speed?
It took about 0.01s for encoding, 0.005s for decoding + presenting the drawing below. (156 strokes) https://user-images.githubusercontent.com/31824270/90310914-efa1bd00-df30-11ea-9d46-4acd290204e7.jpeg
Cool. Can you open a PR?
Could you check my PR?