0

I have a line chart that made with UIBezier path. Here is a code of my draw graph function

override func draw(_ rect: CGRect) {
    let width = rect.width
    let height = rect.height
    
    let path = UIBezierPath(
        roundedRect: rect,
        byRoundingCorners: .allCorners,
        cornerRadii: Constants.cornerRadiusSize
    )
    path.addClip()
    
    guard let context = UIGraphicsGetCurrentContext() else {
        return
    }
    let colors = [startColor.cgColor, endColor.cgColor]
    
    let colorSpace = CGColorSpaceCreateDeviceRGB()
    
    let colorLocations: [CGFloat] = [0.0, 1.0]
    
    guard let gradient = CGGradient(
        colorsSpace: colorSpace,
        colors: colors as CFArray,
        locations: colorLocations
    ) else {
        return
    }
    
    let startPoint = CGPoint.zero
    let endPoint = CGPoint(x: 0, y: bounds.height)
    context.drawLinearGradient(
        gradient,
        start: startPoint,
        end: endPoint,
        options: []
    )
    
    let margin = Constants.margin
    let graphWidth = width - margin * 2 - 4
    let columnXPoint = { (column: Int) -> CGFloat in
      // Calculate the gap between points
      let spacing = graphWidth / CGFloat(self.graphPoints.count - 1)
      return CGFloat(column) * spacing + margin + 2
    }
    
    let topBorder = Constants.topBorder
    let bottomBorder = Constants.bottomBorder
    let graphHeight = height - topBorder - bottomBorder
    guard let maxValue = graphPoints.max() else {
      return
    }
    let columnYPoint = { (graphPoint: Int) -> CGFloat in
      let yPoint = CGFloat(graphPoint) / CGFloat(maxValue) * graphHeight
      return graphHeight + topBorder - yPoint // Переворот графика
    }
    
    UIColor.white.setFill()
    UIColor.white.setStroke()
        
    let graphPath = UIBezierPath()

    graphPath.move(to: CGPoint(x: columnXPoint(0), y: columnYPoint(graphPoints[0])))
        
    for i in 1 ..< graphPoints.count {
      let nextPoint = CGPoint(x: columnXPoint(i), y: columnYPoint(graphPoints[i]))
        print(nextPoint)
      graphPath.addLine(to: nextPoint)
    }

    self.drawAnimatedGraph(path: graphPath, rect)
    
    context.saveGState()
        
    guard let clippingPath = graphPath.copy() as? UIBezierPath else {
      return
    }
        
    clippingPath.addLine(to: CGPoint(
      x: columnXPoint(graphPoints.count - 1),
      y: height))
    clippingPath.addLine(to: CGPoint(x: columnXPoint(0), y: height))
    clippingPath.close()
    
    clippingPath.addClip()
    
    let highestYPoint = columnYPoint(maxValue)
    let graphStartPoint = CGPoint(x: margin, y: highestYPoint)
    let graphEndPoint = CGPoint(x: margin, y: bounds.height)
            
    context.drawLinearGradient(
      gradient,
      start: graphStartPoint,
      end: graphEndPoint,
      options: [])
    context.restoreGState()
    
    graphPath.lineWidth = 2.0
    graphPath.stroke()

    for i in 0 ..< graphPoints.count {
      var point = CGPoint(x: columnXPoint(i), y: columnYPoint(graphPoints[i]))
      point.x -= Constants.circleDiameter / 2
      point.y -= Constants.circleDiameter / 2
          
      let circle = UIBezierPath(
        ovalIn: CGRect(
          origin: point,
          size: CGSize(
            width: Constants.circleDiameter,
            height: Constants.circleDiameter)
        )
      )
      circle.fill()
    }
    
    let linePath = UIBezierPath()

    linePath.move(to: CGPoint(x: margin, y: topBorder))
    linePath.addLine(to: CGPoint(x: width - margin, y: topBorder))

    linePath.move(to: CGPoint(x: margin, y: graphHeight / 2 + topBorder))
    linePath.addLine(to: CGPoint(x: width - margin, y: graphHeight / 2 + topBorder))

    linePath.move(to: CGPoint(x: margin, y: height - bottomBorder))
    linePath.addLine(to: CGPoint(x: width - margin, y: height - bottomBorder))
    let color = UIColor(white: 1.0, alpha: Constants.colorAlpha)
    color.setStroke()
        
    linePath.lineWidth = 1.0
    linePath.stroke()
}

But my problem is that I want to draw my chart with animation and I've found code for it

    func drawAnimatedGraph(path: UIBezierPath, _ rect: CGRect) {
    //Create a CAShape Layer
    let pathLayer: CAShapeLayer = CAShapeLayer()
    pathLayer.frame = self.bounds
    pathLayer.path = path.cgPath
    pathLayer.strokeColor = UIColor.red.cgColor
    pathLayer.fillColor = nil
    pathLayer.lineWidth = 2.0
    pathLayer.lineJoin = CAShapeLayerLineJoin.bevel

    //Add the layer to your view's layer
    self.layer.addSublayer(pathLayer)

    //This is basic animation, quite a few other methods exist to handle animation see the reference site answers
    let pathAnimation: CABasicAnimation = CABasicAnimation(keyPath: "strokeEnd")
    pathAnimation.duration = 2.0
    pathAnimation.fromValue = 0
    pathAnimation.toValue = 1
    //Animation will happen right away
    pathLayer.add(pathAnimation, forKey: "strokeEnd")
}

The problem is that I have a timer which removes first element of array of Integers and append random integer. So when im changing data in array and redraw my chart it draws new chart above main chart. So my question is am I able to draw animated line chart using these technologies or I should look on another way of solving that problem?

enter image description here

Ivanius
  • 109
  • 1
  • 2
  • 9

2 Answers2

0

If you're seeing two graphs, that means that you're rendering it twice. The two obvious alternatives are:

  1. You possibly have the addSublayer of the shape layer and the draw(_:) method. One would generally pick one or the other.

    Frequently, once people start using shape layers, they retire their old draw(_:) implementations.

  2. You have called the routine that did addSublayer more than once. Once you've added the shape layer, don't call addSublayer again. You can simply update the path property of the existing CAShapeLayer rather than adding another shape layer.


I would personally retire draw(_:) entirely (or at least the graph line portion, at least), keep a reference to the CAShapeLayer in a property, and when I want to update the graph, just update the path of the CAShapeLayer.

For the sake of completeness, the alternative is to stick with draw(_:) and perform animation by adding a CADisplayLink (a timer optimally scheduled for screen refresh intervals), update properties used by draw(_:), and then call setNeedsDisplay() to trigger draw(_:) to be called again, which will now use the updated parameters.

The all-shape-layer approach is likely easier.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • I focused on the animation code and didn't even notice that the OP was overriding `draw(:_)`. Drawing manually is CPU-intensive, and tends to create choppy animation. In addition to being easier, using shape layers and CAAnimation is much more performant. – Duncan C Aug 24 '21 at 15:43
0

The answer is, it's complicated.

Yes, you should be able to use the animation approach you show (Using a CAShapeLayer and animating it with a CABasicAnimation of the strokeEnd property.)

Your current drawAnimatedGraph() function creates and adds a new shape layer to your view every time it's called.

You will need to refactor that code to use the same shape layer each time it is called rather than creating and adding one each time.

You will also have to handle the fact that path animation only works properly if the starting and ending path has the same number of control points. You can't add new points to your graph while it is being animated. You would need to let the animation finish, then add a new point and animate that new point into place.

In order to do all that you won't be able to just copy/paste animation code you found online. You'll need to understand what that code is doing and integrate it into your app. CAAnimation is complex, kind of confusing, and not very well documented, so there is a pretty big learning curve.

As Rob pointed out in his answer, you are also mixing draw(:_) based code with using a CAShapeLayer and CAAnimation. That is likely to cause problems. I would get rid of the draw(:_) function and use a shape layer to both draw and animate your graph.

Edit:

See my answer here.

Is this the effect you are after?

enter image description here

Duncan C
  • 128,072
  • 22
  • 173
  • 272