Search code examples
swiftcore-graphicsuibezierpathtopojson

Swift CoreGraphics UIBezierPath will not fill interior correctly


I'm trying to draw countries and fill the interior a certain color. My data source is a TopoJSON file, which, in a nutshell, is made up of shapes that reference an array of arcs to create a shape. I convert this into an array of paths, which I then iterate through to draw the country. As you can see in the below screenshot, I'm drawing the correct lines of the outline of the country (Afghanistan).

Afghanistan Outline

However, when I try to use path.fill(), I end up getting the following. Note how the black lines are correct, but the colors go outside and inside haphazardly.

Afghanistan filled

Code

var mapRegion = MapRegion()
var path = mapRegion.createPath()
var origin: CGPoint = .zero

geometry.paths
    .enumerated()
    .forEach { (geoIndex, shape) in

        shape
            .enumerated()
            .forEach { (shapeIndex, coord) in

                guard let coordPoint = coord.double else { return }
                let values = coordinatesToGraphics(x: coordPoint.x, y: coordPoint.y)
                let point = CGPoint(x: values.x, y: values.y)

                if origin == .zero {
                    origin = point
                }

                // Shape is about to be closed
                if shapeIndex != 0 && path.contains(point) {

                    // Close, save path (2)
                    path.addLine(to: origin)
                    // (3) path.close()
                    mapRegion.savePath()

                    // Add to map, reset process
                    canvas.layer.addSublayer(mapRegion)
                    mapRegions.append(mapRegion)
                    mapRegion = MapRegion()
                    path = mapRegion.createPath()

                 }

                 else {
                     if shapeIndex == 0 {
                         path.move(to: point)
                     } else {
                         path.addLine(to: point)
                     }
                 }



             }

}

I've tried exhaustively messing with usesEvenOddFillRule (further reading), but nothing ever changes. I found that Comment (1) above helped resolve an issue where borders were being drawn that shouldn't be. The function savePath() at (2) runs the setStroke(), stroke(), setFill(), fill() functions.

Update: path.close() draws a line that closes the path at the bottom-left corner of the shape, instead of the top-left corner where it first starts drawing. That function closes the "most recently added subpath", but how are subpaths defined?

I can't say for sure whether the problem is my logic or some CoreGraphics trick. I have a collection of paths that I need to stitch together and treat as one, and I believe I'm doing that. I've looked at the data points, and the end of one arc to the beginning of the next are identical. I printed the path I stitch together and I basically move(to:) the same point, so there are no duplicates when I addLine(to:) Looking at the way the simulator is coloring the region, I first guessed maybe the individual arcs were being treated as shapes, but there are only 6 arcs in this example, and several more inside-outside color switches.

I'd really appreciate any help here!


Solution

  • Turns out that using path.move(to:) creates a subpath within the UIBezierPath(), which the fill algorithm seemingly treats as separate, multiple paths (source that led to discovery). The solution was to remove the extra, unnecessary move(to:) calls. Below is the working code and happy result! Thanks!

    var mapRegion = MapRegion()
    var path = mapRegion.createPath()
    path.move(to: .zero)
    var pointsDictionary: [String: Bool] = [:]
    
    geometry.paths
      .enumerated()
      .forEach { (geoIndex, shape) in
    
          shape
              .enumerated()
              .forEach { (shapeIndex, coord) in
    
                  guard let coordPoint = coord.double else { return }
                  let values = coordinatesToGraphics(x: coordPoint.x, y: coordPoint.y)
                  let point = CGPoint(x: values.x, y: values.y)
    
                  // Move to start
                  if path.currentPoint == .zero {
                      path.move(to: point)
                  }
    
                  if shapeIndex != 0 {
    
                      // Close shape
                      if pointsDictionary[point.debugDescription] ?? false {
    
                          // Close path, set colors, save
                          mapRegion.save(path)
                          regionDrawer.drawPath(of: mapRegion)
    
                          // Reset process
                          canvas.layer.addSublayer(mapRegion)
                          mapRegions.append(mapRegion)
                          mapRegion = MapRegion()
                          path = mapRegion.createPath()
                          pointsDictionary = [:]
    
                      }
    
                      // Add to shape
                      else {
                          path.addLine(to: point)
                      }
    
                  }
    
          }
    
    }
    

    enter image description here