What is the best way to save a CGMutablePath as JSON data that can be uploaded to backend?
I'm aware that it could be converted into a Data
object using UIBezierPath
which conforms to NSCoding, then the data could be converted again to a string and saved to backend, but that doesn't seem like a great way. Is there a better way to save this object to backend?
Perhaps you could pull out the huge array of points that make up the path, convert it into a string and save that. Would this be best?
A CGPath
or CGMutablePath
is a very simple data structure. It's an array of path elements. An each path elements is either a move, line, cubicCurve, curve or closeSubpath operation with 0 to 3 points. That's all. There are no additional attributes or variants.
So it's quite straight-forward to translate a path into an array of path element (struct PathElement
) and then encode it as JSON. It results in a JSON that can be easily read with any programming language and works on many graphics systems (incl. iOS/macOS Quartz, Postscript, PDF, Windows GDI+).
The output of the below sample code consists of the printed CGMutablePath
, the generated JSON and the path restored from JSON:
Path 0x10100d960:
moveto (10, 10)
lineto (30, 30)
quadto (100, 100) (200, 200)
curveto (150, 120) (100, 350) (20, 400)
closepath
moveto (200, 200)
lineto (230, 230)
lineto (260, 210)
closepath
[
{
"type" : 0,
"points" : [
[
10,
10
]
]
},
{
"type" : 1,
"points" : [
[
30,
30
]
]
},
{
"type" : 2,
"points" : [
[
100,
100
],
[
200,
200
]
]
},
{
"type" : 3,
"points" : [
[
150,
120
],
[
100,
350
],
[
20,
400
]
]
},
{
"type" : 4
},
{
"type" : 0,
"points" : [
[
200,
200
]
]
},
{
"type" : 1,
"points" : [
[
230,
230
]
]
},
{
"type" : 1,
"points" : [
[
260,
210
]
]
},
{
"type" : 4
}
]
Path 0x10100bd20:
moveto (10, 10)
lineto (30, 30)
quadto (100, 100) (200, 200)
curveto (150, 120) (100, 350) (20, 400)
closepath
moveto (200, 200)
lineto (230, 230)
lineto (260, 210)
closepath
Sample code:
import Foundation
var path = CGMutablePath()
path.move(to: CGPoint(x: 10, y: 10))
path.addLine(to: CGPoint(x: 30, y: 30))
path.addQuadCurve(to: CGPoint(x: 200, y: 200), control: CGPoint(x: 100, y: 100))
path.addCurve(to: CGPoint(x: 20, y: 400), control1: CGPoint(x: 150, y: 120), control2: CGPoint(x: 100, y: 350))
path.closeSubpath()
path.move(to: CGPoint(x: 200, y: 200))
path.addLine(to: CGPoint(x: 230, y: 230))
path.addLine(to: CGPoint(x: 260, y: 210))
path.closeSubpath()
print(path)
let jsonData = encode(path: path)
let jsonString = String(data: jsonData, encoding: .utf8)!
print(jsonString)
print("")
let restoredPath = try! decode(data: jsonData)
print(restoredPath)
func encode(path: CGPath) -> Data {
var elements = [PathElement]()
path.applyWithBlock() { elem in
let elementType = elem.pointee.type
let n = numPoints(forType: elementType)
var points: Array<CGPoint>?
if n > 0 {
points = Array(UnsafeBufferPointer(start: elem.pointee.points, count: n))
}
elements.append(PathElement(type: Int(elementType.rawValue), points: points))
}
do {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
return try encoder.encode(elements)
} catch {
return Data()
}
}
func decode(data: Data) throws -> CGPath {
let decoder = JSONDecoder()
let elements = try decoder.decode([PathElement].self, from: data)
let path = CGMutablePath()
for elem in elements {
switch elem.type {
case 0:
path.move(to: elem.points![0])
case 1:
path.addLine(to: elem.points![0])
case 2:
path.addQuadCurve(to: elem.points![1], control: elem.points![0])
case 3:
path.addCurve(to: elem.points![2], control1: elem.points![0], control2: elem.points![1])
case 4:
path.closeSubpath()
default:
break
}
}
return path
}
func numPoints(forType type: CGPathElementType) -> Int
{
var n = 0
switch type {
case .moveToPoint:
n = 1
case .addLineToPoint:
n = 1
case .addQuadCurveToPoint:
n = 2
case .addCurveToPoint:
n = 3
case .closeSubpath:
n = 0
default:
n = 0
}
return n
}
struct PathElement: Codable {
var type: Int
var points: Array<CGPoint>?
}