如何从 cellForRowAt 调用 draw(_ rect: CGRect) 在自定义 tableview 上绘图?

Posted

技术标签:

【中文标题】如何从 cellForRowAt 调用 draw(_ rect: CGRect) 在自定义 tableview 上绘图?【英文标题】:how to call draw(_ rect: CGRect) from cellForRowAt to draw on the custom tableview? 【发布时间】:2020-10-29 13:46:42 【问题描述】:

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

在自定义 tableview 中,有一个 UIView 与“drawWorkoutChart”类相关联

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!
  

谢谢。

----------- EDIT --- 根据@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
  

自定义表格单元

    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://***.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)
  

最后是draw(_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()
    
  

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

    IndexPath.row 从 cellForRowAt 传递到 customTableCell 视图 (ErgWorkoutCell) 在初始启动期间,打印语句输出这些
 - 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]

    由于某种原因,tableview 只获取最后一个 indexPath.row 传递给 draw(rect) 函数,所有图表本质上都是 只有 1 个图表,重复

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

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

【问题讨论】:

你的全局变量给你带来了麻烦。您的单元格中有 ergLineChartView 和 chartView 属性也令人困惑 @RobertCrabtree chartView 代码实际上并没有做任何事情。断点似乎没有达到这一点。全局变量目前是使数组传递给绘图函数的原因,我想摆脱全局变量,请您帮忙建议一下吗?一直盯着谷歌搜索,但仍在挣扎。 我终于弄清楚出了什么问题。这是一个错误的 IBOutlet 参考。原始代码让我执行 @IBOutlet weak var ergLineChartView: UIView! 引用 UIView!相反,这个 IBoutlet 应该指向我的 drawRect 函数代码。即@IBOutlet weak var ergLineChartView: DrawAllErgWorkoutChart! 完成此操作后,从 cellForRowAt 传递的值将传递给func configure(ergX: [CGFloat], ergY: [CGFloat], ergIndexPassed: [Int]),然后将其传递给 DrawAllErgWorkoutChart! 这篇文章让我仔细检查并验证了我在做什么***.com/a/35252607/14414215 【参考方案1】:

您只需将数据点数组作为公共属性进行访问:

class DrawWorkoutChart: UIView 

    private let ftp = 100

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

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

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

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:) 之前清除您的图表。

configure 方法是魔法发生的地方。我们传入我们的数据点并调用 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: $0) 
    

【讨论】:

尽管查看了您的代码/建议几个小时,但我仍然卡住了。什么有效 - CellForRowAt 可以将 dataPointsX 和 dataPointsY 传递给配置(在 ErgWorkoutCell 内)。但是,它不会传递给 draw(_ rect: CGRect) 即使 dataPointX & dataPointsY 现在像您的示例一样被放置在 draw(rect) 函数之外。当我尝试打印出数组时,我得到“NIL”。我觉得它已经接近了。我也搜索了几个小时,但还没有找到解决方案。 参考您关于显式设置 BG 颜色和 clearsContextBeforeDrawing 的评论与此有关 --> ***.com/questions/39078466/…。还没有弄清楚为什么 dataPointsX 没有被传递给 draw(rect) 代码 @myjunk 也许你可以用你当前的代码更新你的帖子。 根据您的帮助更新了原始帖子(在底部)。非常感谢【参考方案2】:

来自documentation

第一次显示视图或发生事件时调用此方法 发生使视图的可见部分无效。你不应该 自己直接调用这个方法。要使您的部分视图无效, 并因此导致该部分被重绘,调用 setNeedsDisplay() 或 setNeedsDisplay(_:) 方法。

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

struct Point 
    var x: Float
    var y: Float

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


var dataPoints = [Point]()

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

在自定义单元类中,我会声明一个“设置”方法,如下所示:

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

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

cell.setup(dataPoints)

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

【讨论】:

感谢您的建议。数组实际上已经存在于 JSON (sonErgWorkouts[indexPath.row].intervals) 中。自定义类是“drawWorkoutChart”。我现在缺少的是您所描述的“设置”方法。您能否详细说明这一点或指出我如何获得此设置方法的方向?感谢指导。 我编辑了我的帖子。我认为您也可以为自定义类执行“设置”方法,并在初始化 drawWorkoutChart 对象时从单元格的设置中调用它。 感谢@Cora 提供更新的帮助。但是,我太新手了,无法真正知道我应该做什么才能使其正常工作。 (在高层次上,我明白你在说什么,但我不太知道如何编码:-() docs.swift.org 是一个很好的起点。谷歌“声明继承自 UIView 的自定义类”,如何初始化并将它们添加到层次结构中。 对于 contentView.addSubview(customView),customView 是我在 customClass 中为 UIView 定义的 IBOutlet (ergLineChartView)?无论如何,我这样做了,然后复制了绘图函数(从 func point(at ix: Int) -> CGPoint 一直到最后,就像你建议的那样),但没有出现。 (可能是因为 customCell 中的 UIView 不知道它与任何类相关联?

以上是关于如何从 cellForRowAt 调用 draw(_ rect: CGRect) 在自定义 tableview 上绘图?的主要内容,如果未能解决你的问题,请参考以下文章

未调用 cellForRowAt 但正在调用 numberOfSection 和 numberOfRowsInSection

numberOfRowsInSection 已调用但 cellForRowAt 未调用

为啥 UITableViewCells cellForRowAt 方法不调用?

UITableView,cellForRowAt indexPath 没有被调用

未使用 reloadData() 方法调用 cellForRowAt indexPath

UITableView "cellForRowAt: indexPath" 偶尔在 Core Data 属性上调用 "init?(coder aDecoder: NSCo