swiftuibezierpathdrawrect

bezierPath setfill with diff colours Not Working ( to create a mountain climb color coding )


My goal is to create a chart much like below. Where the diff colours represent the different slope / gradient % for each segment.

enter image description here

My previous code, I have already managed to get the "mountain" outline, now further improved it such that I am now making a set of "boxes" which represents each colour segment. But am facing some issue.

Here is the code I'm using right now.

    let myBezier = UIBezierPath()
    let nextPt = fillSlopes(at: idx)

    if idx > 0 {
      let prevPt = fillSlopes(at: idx - 1)
      myBezier.move(to: CGPoint(x: prevPt.x, y: height))
      myBezier.addLine(to: CGPoint(x: prevPt.x, y: prevPt.y))
      myBezier.addLine(to: CGPoint(x: nextPt.x, y: nextPt.y))
      myBezier.addLine(to: CGPoint(x: nextPt.x, y: height))
      myBezier.addLine(to: CGPoint(x: prevPt.x, y: height))

      myBezier.close()
      // This is test code, actual code will compare current slope % 
      // (in an array) and then based on the slope %, will change
      // the colour accordingly.
      if idx % 2 == 0 {
        UIColor.systemRed.setFill()
        myBezier.fill()
      } else {
        UIColor.systemBlue.setFill()
        myBezier.fill()
      }

This is the results. Unfortunately, the colours is not coming out properly. What Am I missing?

enter image description here

In addition to this, Drawing so many little boxes is really eating up a lot of CPU cycles, if there are other suggestions to get the desired output result is also appreciated. Thanks

Update: Many Thanks for @seaspell, I realised what was happening. I was putting my let myBezier = UIBezierPath() outside of the for loop hence my [UIBezierPath] array was getting mangled up. eg:

This got things all wrong:

func xxx { 
  let myBezier = UIBezierPath() // <<<<<< this is wrong
      
  for idx in slopeBars.indices { 
      // do stuff 
  }

This is Correct and fixed some performance issue as well:

func xxx { 
  var slopeBars = [UIBezierPath]()
  var slopeBarColorArray = [UIColor]()

  for idx in slopeBars.indices { 
    let myBezier = UIBezierPath()  // <<<<< Moved here
    let nextPt = fillSlopes(at: idx)

    if idx > 0 {
      let prevPt = fillSlopes(at: idx - 1)
      myBezier.move(to: CGPoint(x: prevPt.x, y: height))
      myBezier.addLine(to: CGPoint(x: prevPt.x, y: prevPt.y))
      myBezier.addLine(to: CGPoint(x: nextPt.x, y: nextPt.y))
      myBezier.addLine(to: CGPoint(x: nextPt.x, y: height))
      myBezier.addLine(to: CGPoint(x: prevPt.x, y: height))

      myBezier.close()
      slopeBars.append(myBezier)

        if gradient < 0.0 {
          slopeBarColorArray.append(UIColor(cgColor: Consts.elevChart.slope0_0))
        } else if gradient < 4.0 {
          slopeBarColorArray.append(UIColor(cgColor: Consts.elevChart.slope0_4))
       }

    for i in 0..<slopeBars.count {
      if !slopeBarColorArray.isEmpty {
        slopeBarColorArray[i].setStroke()
        slopeBarColorArray[i].setFill()
      }
    
      slopeBars[i].stroke()
      slopeBars[i].fill()
    }
  }

enter image description here


Solution

  • There's nothing really wrong with using bezier paths for this. You need to use a new one for each bar though and also you don't want to build them in draw(rect:). I suspect that is the cause of the CPU issues.

    Take a look at this example I put together.

       class BarChartView: UIView {
    
        lazy var dataPoints: [CGFloat] = {
            var points = [CGFloat]()
            for _ in 1...barCount {
                let random = arc4random_uniform(UInt32(bounds.maxY / 2))
                points.append(CGFloat(random))
            }
            return points
        }()
    
        var bars = [UIBezierPath]()
        private let barCount = 20
    
        lazy var colors: [UIColor] = {
            let colors:  [UIColor] = [.red, .blue, .cyan, .brown, .darkGray, .green, .yellow, .gray, .lightGray, .magenta, .orange, .purple, .systemPink, .white]
            var random = [UIColor]()
    
            for i in 0...barCount {
                random.append(colors[Int(arc4random_uniform(UInt32(colors.count)))])
            }
    
            return random
        }()
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            self.bars = buildBars()
        }
    
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            self.bars = buildBars()
        }
    
    
        func buildBars() -> [UIBezierPath] {
    
            let lineWidth = Int(bounds.maxX / CGFloat(barCount))
    
            var bars = [UIBezierPath]()
    
            for i in 0..<barCount {
    
                let line = UIBezierPath()
    
                let leftX = lineWidth * i
                let rightX = leftX + lineWidth
                let centerX = leftX + (lineWidth / 2)
    
                let nextLeftPoint = (i == 0 ? dataPoints[i] : dataPoints[i - 1])
                let nextRightPoint = (i == barCount - 1 ? dataPoints[i] : dataPoints[i + 1])
    
                let currentPoint = dataPoints[i]
    
    
                //bottom left
                line.move(to:  CGPoint(x: leftX, y: Int(bounds.maxY)) )
                //bottom right
                line.addLine(to: CGPoint(x: rightX, y: Int(bounds.maxY)) )
    
                //top right
                line.addLine(to: CGPoint(x: rightX,  y: Int((currentPoint + nextRightPoint) / 2)) )
    
                //top center
                line.addLine(to: CGPoint(x: centerX, y: Int(currentPoint)) )
    
                //top left
                line.addLine(to: CGPoint(x: leftX,  y: Int((currentPoint + nextLeftPoint) / 2)) )
    
                //close the path
                line.close()
    
                bars.append(line)
            }
    
            return bars
        }
    
        override func draw(_ rect: CGRect) {
            super.draw(rect)
    
            for i in 0..<bars.count {
                colors[i].setStroke()
                colors[i].setFill()
    
                bars[i].stroke()
                bars[i].fill()
            }
        }
    }