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?
A couple of thoughts:
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
.
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.)
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.
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.
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
.