如何从 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