0
votes

I currently have draw(_ rect: CGRect) working independent from the tableview (meaning, the xy points are all hardcoded). I am trying to put this draw method into my custom UITableview such that it will draw the individual charts in each of the cell.

In the custom tableview, there is one UIView in which I have associated w/ the "drawWorkoutChart" class

import UIKit

let ftp = 100

class drawWorkoutChart: UIView {
  
  
  override func draw(_ rect: CGRect) {
    
    let dataPointsX: [CGFloat] = [0,10,10,16,16,18]
    let dataPointsY: [CGFloat] = [0,55,73,73,52,52]
    
    func point(at ix: Int) -> CGPoint {
      
      let pointY = dataPointsY[ix]
      let x = (dataPointsX[ix] / dataPointsX.max()!) * rect.width
      let yMax = dataPointsY.max()! > 2*CGFloat(ftp) ? dataPointsY.max()! : 2*CGFloat(ftp)
      let y = (1 - ((pointY - dataPointsY.min()!) / (yMax - dataPointsY.min()!))) * rect.height
//    print("width:\(rect.width) height:\(rect.height) ix:\(ix) dataPoint:\(pointY) x:\(x) y:\(y) yMax:\(yMax)")
      return CGPoint(x: x, y: y)
    }
    
    func drawFtpLine() -> CGFloat {
      let yMax = dataPointsY.max()! > 2*CGFloat(ftp) ? dataPointsY.max()! : 2*CGFloat(ftp)
      let ftpY =  (CGFloat(ftp) / yMax ) * rect.height
      return ftpY
    }
    
    //Here's how you make your curve...
    let myBezier = UIBezierPath()
    myBezier.move(to: CGPoint(x: 0, y: (1 - dataPointsY[0]) * rect.height))
    
    for idx in dataPointsY.indices {
      myBezier.addLine(to: point(at: idx))
    }
//    UIColor.systemBlue.setFill()
//    myBezier.fill()
    
    UIColor.systemBlue.setStroke()
    myBezier.lineWidth = 3
    myBezier.stroke()
    
    let ftpLine = UIBezierPath()
    ftpLine.move(to: CGPoint(x: 0, y: drawFtpLine()))
    ftpLine.addLine(to: CGPoint(x: rect.width, y: drawFtpLine()))
    UIColor.systemYellow.setStroke()
    ftpLine.lineWidth = 2
    ftpLine.stroke()
  }
}

Once this is done, running the app will result in multiples of these charts drawn on the UIView

PIC: The UIVIew Chart that gets drawn on each cell in the tableview (Not enough reputation for it to show inline

The help I need (after scouring thru stack overflow / YouTube and webpages from YouTube) is that I still do not know how to call it from within cellForRowAt such that I can replace the dataPointX and dataPointY appropriately and have it draw each cell differently based on the input data.

currently the UITableViewCell has this:

extension VCLibrary: UITableViewDataSource{
  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return jsonErgWorkouts.count
  }
  
  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    
    // this is cast AS! ErgWorkoutCell since this is a custom tableViewCell
    let cell = tableView.dequeueReusableCell(withIdentifier: "ergWorkoutCell") as? ErgWorkoutCell
    
    cell?.ergWorkoutTitle.text = jsonErgWorkouts[indexPath.row].title
    cell?.ergWorkoutTime.text = String(FormatDisplay.time(Int(jsonErgWorkouts[indexPath.row].durationMin * 60)))
    cell?.ergValue.text = String(jsonErgWorkouts[indexPath.row].value)
    cell?.ergWorkoutIF.text = String(jsonErgWorkouts[indexPath.row].intensity)
    
    return cell!
  }
}

Thanks.

----------- EDIT --- Update per @Robert C ----

UITableViewCell cellForRowAt

      func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    // this is cast AS! ErgWorkoutCell since this is a custom tableViewCell
    let cell = tableView.dequeueReusableCell(withIdentifier: "ergWorkoutCell") as! ErgWorkoutCell

    cell.ergWorkoutTitle.text = jsonErgWorkouts[indexPath.row].title + "<><>" + String(jsonErgWorkouts[indexPath.row].id)

    var tempX: [CGFloat] = []
    var tempY: [CGFloat] = []
    
    jsonErgWorkouts[indexPath.row].intervals.forEach {
      tempX.append(CGFloat($0[0] * 60))
      tempY.append(CGFloat($0[1]))
    }

    // I used ergIndexPassed as a simple tracking to see which cell is updating
    cell.configure(ergX: tempX, ergY: tempY, ergIndexPassed: [indexPath.row])
//    cell.ergLineChartView.setNeedsDisplay() // <- This doesn't make a diff
  return cell
  }
}

The CustomTableCell

    import UIKit

extension UIView {
    func fill(with view: UIView) {
        addSubview(view)
        NSLayoutConstraint.activate([
            leftAnchor.constraint(equalTo: view.leftAnchor),
            rightAnchor.constraint(equalTo: view.rightAnchor),
            topAnchor.constraint(equalTo: view.topAnchor),
            bottomAnchor.constraint(equalTo: view.bottomAnchor),
        ])
    }
}




class ErgWorkoutCell: UITableViewCell {

  @IBOutlet weak var ergWorkoutTitle: UILabel!
  @IBOutlet weak var ergWorkoutTime: UILabel!
  @IBOutlet weak var ergWorkoutStress: UILabel!
  @IBOutlet weak var ergWorkoutIF: UILabel!
  @IBOutlet weak var ergLineChartView: UIView!


    static let identifier = String(describing: ErgWorkoutCell.self)

    // These code doesn't seem to Get triggered
    lazy var chartView: DrawAllErgWorkoutChart = {
        let chart = DrawAllErgWorkoutChart()
        chart.translatesAutoresizingMaskIntoConstraints = false
      chart.backgroundColor = .blue
        chart.clearsContextBeforeDrawing = true

        let height = chart.heightAnchor.constraint(equalToConstant: 500)
        height.priority = .defaultHigh
        height.isActive = true
        return chart
    }()

  // I changed this to also accept the ergIndex
  func configure(ergX: [CGFloat], ergY: [CGFloat], ergIndexPassed: [Int]) {    
      dataPointsX = ergX
      dataPointsY = ergY
      ergIndex = ergIndexPassed
    // This works. ergLineChartView is the Embedded UIView in the customCell
    ergLineChartView.setNeedsDisplay()

// These does nothing
//    let chart = DrawAllErgWorkoutChart()
//    chartView.setNeedsDisplay()
//    chart.setNeedsDisplay()

      print("ergWorkoutCell ergIndex:\(ergIndex)")//" DPY:\(dataPointsY)")
    }

  
  override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
    super.init(style: style, reuseIdentifier: reuseIdentifier)
    contentView.fill(with: chartView)
  }
  
  
//  https://stackguides.com/questions/38966565/fatal-error-initcoder-has-not-been-implemented-error-despite-being-implement
//  required init?(coder: NSCoder) {
//      fatalError("init(coder:) has not been implemented")
//  }

  required init?(coder aDecoder: NSCoder) {
     super.init(coder: aDecoder)
  }
}

Finally the draw(_ rect) code

import UIKit

// once I placed the dataPoints Array declaration here, then it works. This is a Global tho
var dataPointsX: [CGFloat] = []
var dataPointsY: [CGFloat] = []
var ergIndex: [Int] = []

class DrawAllErgWorkoutChart: UIView {
  private let ftp = 100
  
  override func draw(_ rect: CGRect) {
    super.draw(rect)
    
    let yMax = dataPointsY.max()! > 2*CGFloat(ftp) ? dataPointsY.max()! : 2*CGFloat(ftp)
    
    print("DrawAllErgWorkoutChart ergIndex:\(ergIndex) ") //time:\(dataPointsX) watt:\(dataPointsY)")
    
    func point(at ix: Int) -> CGPoint {
      
      let pointY = dataPointsY[ix]
      let x = (dataPointsX[ix] / dataPointsX.max()!) * rect.width
      let y = (1 - (pointY / yMax)) * rect.height
      return CGPoint(x: x, y: y)
    }
    
    func drawFtpLine() -> CGFloat {
      let ftpY =  (CGFloat(ftp) / yMax ) * rect.height
      return ftpY
    }
    
    //Here's how you make your curve...
    let myBezier = UIBezierPath()
    let startPt = dataPointsY[0]
    let nStartPt = (1 - (startPt / yMax)) * rect.height
    //    print("Cnt:\(ergLibCounter) StartPt:\(startPt) nStartPt:\(nStartPt)")
    //    myBezier.move(to: CGPoint(x: 0, y: (1 - dataPointsY[0]) * rect.height))
    myBezier.move(to: CGPoint(x: 0, y: nStartPt))
    
    for idx in dataPointsY.indices {
      myBezier.addLine(to: point(at: idx))
    }
    
    
    UIColor.systemBlue.setStroke()
    myBezier.lineWidth = 3
    myBezier.stroke()
    
    let ftpLine = UIBezierPath()
    ftpLine.move(to: CGPoint(x: 0, y: drawFtpLine()))
    ftpLine.addLine(to: CGPoint(x: rect.width, y: drawFtpLine()))
    UIColor.systemRed.setStroke()
    ftpLine.lineWidth = 2
    ftpLine.stroke()
    
  }
}

With the new Code above, what works:

  1. The IndexPath.row gets passed from cellForRowAt to the
    customTableCell view (ErgWorkoutCell)
  2. During initial launch, the print statements outputs these
 - ergWorkoutCell ergIndex:[1]
 - ergWorkoutCell ergIndex:[2]
 - ergWorkoutCell ergIndex:[3]
 - ergWorkoutCell ergIndex:[4]
 - ergWorkoutCell ergIndex:[5]
 - ergWorkoutCell ergIndex:[6]
 - DrawAllErgWorkoutChart ergIndex:[6]
 - DrawAllErgWorkoutChart ergIndex:[6]
 - DrawAllErgWorkoutChart ergIndex:[6]
 - DrawAllErgWorkoutChart ergIndex:[6]
 - DrawAllErgWorkoutChart ergIndex:[6]
 - DrawAllErgWorkoutChart ergIndex:[6]
 - DrawAllErgWorkoutChart ergIndex:[6]
  1. for some reason, the tableview only gets the last indexPath.row passed to the draw(rect) function and all the charts are essentially just 1 chart, repeated

  2. Once I start scrolling, then the charts gets re-populated to the correct charts.

  3. Here is the entire project which may make it easier to see what's going on https://www.dropbox.com/s/3l7r7saqv0rhfem/uitableview-notupdating.zip?dl=0

2
Your global variables are getting you into trouble. it's also confusing that you have ergLineChartView and chartView properties in your cellRob
@RobertCrabtree The chartView code isn't really doing anything. Breakpoints doesn't seems to hit that. The globals currently is what makes the array get passed to the draw function, I would like to get rid of the globals, could you please help advise? Been staring and googling and still struggling.app4g
I finally figured out what was wrong. It was a wrong IBOutlet reference. The original code had me doing @IBOutlet weak var ergLineChartView: UIView! which was referencing an UIView! where instead this IBoutlet should be pointed to my drawRect function code. namely @IBOutlet weak var ergLineChartView: DrawAllErgWorkoutChart! once this is done, then the values passed from cellForRowAt will get passed to func configure(ergX: [CGFloat], ergY: [CGFloat], ergIndexPassed: [Int]) which then passes it to DrawAllErgWorkoutChart!app4g
This was the post that led me to double check and verify what I was doing stackoverflow.com/a/35252607/14414215app4g

2 Answers

1
votes

You just need to make your data point arrays accessible as public properties:

class DrawWorkoutChart: UIView {

    private let ftp = 100

    var dataPointsX: [CGFloat] = []
    var dataPointsY: [CGFloat] = []

    override func draw(_ rect: CGRect) {
        super.draw(rect)
    }
}

In your custom Cell class you need a custom method to pass that data points to your view:

extension UIView {
    func fill(with view: UIView) {
        addSubview(view)
        NSLayoutConstraint.activate([
            leftAnchor.constraint(equalTo: view.leftAnchor),
            rightAnchor.constraint(equalTo: view.rightAnchor),
            topAnchor.constraint(equalTo: view.topAnchor),
            bottomAnchor.constraint(equalTo: view.bottomAnchor),
        ])
    }
}

class ErgWorkoutCell: UITableViewCell {

    static let identifier = String(describing: ErgWorkoutCell.self)

    lazy var chartView: DrawWorkoutChart = {
        let chart = DrawWorkoutChart()
        chart.translatesAutoresizingMaskIntoConstraints = false
        chart.backgroundColor = .black
        chart.clearsContextBeforeDrawing = true

        let height = chart.heightAnchor.constraint(equalToConstant: 500)
        height.priority = .defaultHigh
        height.isActive = true
        return chart
    }()

    func configure(dataPointsX: [CGFloat], dataPointsY: [CGFloat]) {
        chartView.dataPointsX = dataPointsX
        chartView.dataPointsY = dataPointsY
        chartView.setNeedsDisplay()
    }

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        contentView.fill(with: chartView)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

Notice that I'm explicitly setting the view's backgroundColor and clearsContextBeforeDrawing properties. This is important so that your your chart is cleared before draw(rect:) is called.

The configure method is where the magic happens. We pass in our data points and call setNeedsDisplay so that the view is redrawn.

Now in your cellForRowAt method you just need to pass in your data points:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: ErgWorkoutCell.identifier) as! ErgWorkoutCell
    cell.configure(
        dataPointsX: .random(count: 6, in: 0..<20),
        dataPointsY: .random(count: 6, in: 0..<100)
    )
    return cell
}

And this is just a method for generating arrays with random values:

extension Array where Element: SignedNumeric {

    static func random(count: Int, in range: Range<Int>) -> Self {
        return Array(repeating: 0, count: count)
            .map { _ in Int.random(in: range) }
            .compactMap { Element(exactly: $0) }
    }
}
0
votes

From documentation

This method is called when a view is first displayed or when an event occurs that invalidates a visible part of the view. You should never call this method directly yourself. To invalidate part of your view, and thus cause that portion to be redrawn, call the setNeedsDisplay() or setNeedsDisplay(_:) method instead.

To make things easier I would declare a struct that looks like this:

struct Point {
    var x: Float
    var y: Float

    init(coordX: Float, coordY: Float) {
        x = coordX
        y = coordY
    }
}

var dataPoints = [Point]()

then I would populate dataPoints with the data from your json array instead of creating two separate arrays.

In the custom cell class I would declare a "setup" method like this:

func setup(dataArray: [Point]) {
    // setup customView with drawing
    contentView.addSubview(customView)
}

and move the code from draw to setup, which can be called from cellForRowAt() like this,

cell.setup(dataPoints)

Hopefully, you will be able to use some of this.