Search code examples
swiftcore-graphicsuibezierpath

Fast alternatives for UIBezierPath


I'm making app, that can build graphs. I'm working with UIBezierPath, and everything was fine until I decided to make it possible to move and scale the graph using gestures.

here's how i'm drawing:

var isFirstPoint = true
        color.set()
        funcPath.lineWidth = width

        for i in 0...Int(bounds.size.width * contentScaleFactor) {
            let cgX = CGFloat(i) / contentScaleFactor
            let funcX = Double ((cgX - bounds.width / 2) / scale)

            let funcY = function(funcX) * Double(scale)

            guard !funcY.isNaN else { continue }
            let cgY = -( CGFloat(funcY) - bounds.height / 2)

            if isFirstPoint {
                funcPath.move(to: CGPoint(x: cgX, y: cgY))
                isFirstPoint = false
            } else {
                if cgY > max(bounds.size.width, bounds.size.height) * 2 {
                    isFirstPoint = true
                } else {
                    funcPath.addLine(to: CGPoint(x: cgX, y: cgY) )
                }
            }
        }
funcPath.stroke()

is there is a faster way to do it?


Solution

  • A couple of thoughts:

    1. Rebuilding the path for every pixel can be an expensive process. One way to reduce this cost is to reduce the number of data points you render. For example, if you’re adjusting scale and rebuilding the whole path, you might want to use some step factor when updating the path mid-gesture, and replace your for loop with:

      let step = 4
      for i in stride(from: 0, through: Int(bounds.size.width * contentScaleFactor), by: step) {
          ...
      }
      

      Then, when the gesture ends, you might update the path one more time, with a step of 1.

    2. The other way to do this is to not update the scale and call setNeedsDisplay each time, but rather to just update the transform of the view. (This can result in super fast behavior when zooming in, but is obviously problematic when zooming out, as portions excluded from the path won’t be rendered.)

    3. If you’re testing this, make sure to test release (i.e. optimized) build on physical device. Debug builds, with all of their safety checks, can really slow down the process, which will stand out with something as computationally intensive as this.

    4. By the way, it looks like the building of the path is buried in draw(rect:). I’d decouple the building of the UIBezierPath from the stroking of the path because while, yes, every time you update the function, you may want to update and draw the path, you definitely don’t want re-build the path every time draw(rect:) is called.

    5. Once you do that, the draw(rect:) might not even be needed any. You might just add a CAShapeLayer as sublayer of the view’s layer, set the strokeColor and strokeWidth of that shape layer, and then update its path.