1

I have a buffer that gets filled with float data points every 1/10 of a second. When the data is ready/comes (around 4000 each time), I translate that data into lines.

My problem here, as you may already know, is that the drawing becomes really slow. I'm posting some of my code below.

Here's my custom UIView. This method gets called once.

override func layoutSubviews() {
    super.layoutSubviews()

    // init stuff for drawing animation
    path = UIBezierPath()
    yOff = Float(self.bounds.height / 2)
    lastPoint = CGPoint(x: 1.0, y: Double(yOff))
    path.move(to: lastPoint)
    path.addLine(to: lastPoint)

    pathLayer = CAShapeLayer()
    pathLayer.frame = CGRect(x: 0, y: 0, width: self.bounds.width, height: self.bounds.height)
    pathLayer.path = path.cgPath
    pathLayer.strokeColor = UIColor.red.cgColor
    pathLayer.fillColor = nil
    pathLayer.lineWidth = 0.2
    pathLayer.lineJoin = kCALineJoinBevel


    let pathAnimation: CABasicAnimation = CABasicAnimation(keyPath: "strokeEnd")
    pathAnimation.duration = 0.1
    pathAnimation.fromValue = NSNumber(value: 0.0)
    pathAnimation.toValue = NSNumber(value:1.0)


    self.layer.addSublayer(pathLayer)


    pathLayer.add(pathAnimation, forKey: "strokeEnd")
}

The following method does the actual drawing, which gets called by "setNeedsDisplay" every 1/10 of a second.

override func draw(_ rect: CGRect) {

    // this is where the magic (animation) happens
    CATransaction.begin()
    pathLayer.path = path.cgPath
    CATransaction.commit()

}

This is in the viewcontroller. I'm looping through the data points to create a CGPoint for each data. Probably inefficient; any help/idea is welcome:

for _s in samples
{
    let currP = CGPoint(x: cX * xModifier, y: yOffSet + (CGFloat(_s)*yModifier))
    cX += xLen
    self.waveView.path.addLine(to: currP)
    ctr = ctr + 1
}

DispatchQueue.main.async {
    self.waveView.setNeedsDisplay()
}

Thanks in advance.

GonnaGetGet
  • 254
  • 2
  • 12
  • 1
    @Rob I will try to remove `draw` and see if it changes anything. Regarding `layoutSubviews`, that method seem to be called only once - I did some tests. Are you certain? Thanks! – GonnaGetGet Aug 01 '17 at 14:49

1 Answers1

1

A couple of thoughts:

  1. Don't update layer in draw. You should eliminate that method entirely, as it's only used when you're stroking your own path. But you're using CAShapeLayer, which eliminates the need to do any of that. When you update your path, replace the call to setNeedsDisplay with code that just updates the pathLayer.path directly. If you want to animate, too, then perform you CABasicAnimation here, too.

  2. Unrelated to your current issue, you should be careful about adding layers in layoutSubviews. That method can get called multiple times. Maybe you’re only seeing it called once right now, but it can be called repeatedly. E.g., if you rotate device, it can get called. If you use autolayout, and do something that triggers autolayout engine, it can get called. As a general rule, it can be called multiple times (even if you're only seeing it called only once right now).

  3. Adding 4000 line segments to a path is quite a bit and it can be slow to render. It's not entirely clear from your question with what frequency you're adding another 4000 line segments.

    Often when we want to add to an existing (potentially long) bezier path, we instead take a snapshot of the existing view, render that in a UIImageView, and then just render the incremental portion of the path in the CAShapeLayer. This ensures a consistent performance pattern, just the time to render image and the incremental portion, rather than trying to re-render an increasingly long UIBezierPath.

Community
  • 1
  • 1
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Hi Rob. Regarding #3 - I'm adding 4000 line segments every 1/10 of a second, so 40,000 per second. Your last point makes a lot of sense. Can you point me to the direction on how to take a snapshot of the existing view? Thanks again! – GonnaGetGet Aug 01 '17 at 16:54
  • @GonnaGetGet - See `constructIncrementalImage` in https://stackoverflow.com/a/34997902/1271826 as an example. Now, that answer is addressing a different question, but the "create/update incremental image" concept is largely the same. And in your case, because you're using `CAShapeLayer`, you'd just have `UIImageView` rather than manually drawing it in `draw(_ rect:)`, but hopefully it illustrates the idea. – Rob Aug 01 '17 at 17:29
  • @GonnaGetGet - That having been said, drawing 40,000 points per second (esp on a device capable of (generally) 60 fps and resolution nowhere near 40,000 pixels in either dimension) may still be a challenge. I'd take a hard look at whether you might simplify/reduce the data set before rendering. (E.g., keep the raw data if that's useful for other purposes, but perhaps stride through the array of points for rendering purposes.) – Rob Aug 01 '17 at 17:37
  • I will try that answer, thanks. I actually tried implementing the following extension: `UIGraphicsBeginImageContext(self.frame.size); img.draw(in: self.bounds); let patternImage: UIImage = UIGraphicsGetImageFromCurrentImageContext()!; UIGraphicsEndImageContext(); self.backgroundColor = UIColor(patternImage: patternImage);` – GonnaGetGet Aug 01 '17 at 17:57
  • First, use `UIGraphicsBeginImageContextWithOptions` so you don't lose retina quality. Second, what is `img`? Third, I'm not sure why you're using `patternImage` pattern rather than just using `UIImageView`. – Rob Aug 01 '17 at 18:00
  • Ahh yes, I forgot that I'm implementing it as extension, so `self` would've done it. I just found this code somewhere before you linked your other answer. Trying yours now. (: – GonnaGetGet Aug 01 '17 at 18:06
  • Taking a snapshot of the current view and using it as the background did the trick. Thanks @Rob! – GonnaGetGet Aug 02 '17 at 15:28