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
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://stackoverflow.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:
- 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]
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
Once I start scrolling, then the charts gets re-populated to the correct charts.
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
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) }
}
}