如何从 cellForRowAt 调用 draw(_ rect: CGRect) 来绘制自定义 tableview?

how to call draw(_ rect: CGRect) from cellForRowAt to draw on the custom tableview?

我目前有 draw(_ rect: CGRect) 独立于 tableview 工作(意思是,xy 点都是硬编码的)。我正在尝试将此绘制方法放入我的自定义 UITableview 中,以便它将在每个单元格中绘制单独的图表。

在自定义表格视图中,有一个 UIView,我在其中关联了“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()
  }
}

一旦完成,运行 应用程序将在 UIView 上绘制多个这些图表

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

我需要的帮助(在通过堆栈溢出/YouTube 和来自 YouTube 的网页搜索之后)是我仍然不知道如何从 cellForRowAt 中调用它以便我可以适当地替换 dataPointX 和 dataPointY 并让它根据输入数据以不同方式绘制每个单元格。

目前 UITableViewCell 有这个:

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!
  }
}

谢谢。

------------ 编辑 --- 根据@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([=12=][0] * 60))
      tempY.append(CGFloat([=12=][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
  }
}

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)
  }
  
  
//  
//  required init?(coder: NSCoder) {
//      fatalError("init(coder:) has not been implemented")
//  }

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

最后绘制(_ rect)代码

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()
    
  }
}

使用上面的新代码,有效的是:

  1. IndexPath.row 从 cellForRowAt 传递到
    customTableCell 视图 (ErgWorkoutCell)
  2. 在初始启动期间,打印语句输出这些
 - 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. 由于某些原因,tableview 只获取最后一个 indexPath.row 传递给 draw(rect) 函数,所有图表本质上都是 只有 1 个图表,重复

  2. 一旦我开始滚动,图表就会重新填充到 正确的图表。

  3. 这是整个项目,可以更容易地看到是什么 进行中 https://www.dropbox.com/s/3l7r7saqv0rhfem/uitableview-notupdating.zip?dl=0

来自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.

为了让事情更简单,我会声明一个如下所示的结构:

struct Point {
    var x: Float
    var y: Float

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

var dataPoints = [Point]()

然后我将使用您的 json 数组中的数据填充数据点,而不是创建两个单独的数组。

在自定义单元格中 class 我会像这样声明一个“设置”方法:

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

并将代码从 draw 移动到 setup,可以从 cellForRowAt() 像这样,

cell.setup(dataPoints)

希望您能够使用其中的一些内容。

您只需要使您的数据点数组可以作为 public 属性访问:

class DrawWorkoutChart: UIView {

    private let ftp = 100

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

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

在您的自定义 Cell class 中,您需要一个自定义方法来将数据点传递给您的视图:

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")
    }
}

请注意,我明确设置了视图的 backgroundColor 和 clearsContextBeforeDrawing 属性。这很重要,以便在调用 draw(rect:) 之前清除图表。

配置方法是魔法发生的地方。我们传入我们的数据点并调用 setNeedsDisplay 以便重绘视图。

现在,在您的 cellForRowAt 方法中,您只需传入数据点:

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
}

这只是一种生成随机值数组的方法:

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: [=13=]) }
    }
}