Search code examples
swiftcgpath

Swift Best Way to Save CGMutablePath as JSON


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?


Solution

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