下面是“iOS实现的多条折线图封装实例”的完整攻略。
1. 需求分析
在开始进行多条折线图封装前,我们需要明确需求,分析出我们所需要的功能和特性。
1.1 功能需求
- 绘制多条折线图;
- 支持同时显示多个数据源;
- 支持自定义颜色、线型、数据点形状等设置;
- 支持显示数据点的数值;
- 支持动画效果。
1.2 技术需求
- 使用 Core Graphics 绘制折线图;
- 使用 UIBezierPath 绘制曲线;
- 使用 CAShapeLayer 实现折线图动画效果;
- 使用 UICollectionView 实现数据源的展示;
- 使用 UICollectionViewFlowLayout 实现数据源的布局。
2. 技术实现
在满足需求的前提下,下面我们介绍如何实现这个多条折线图的封装。
2.1 开发环境
- macOS Catalina 10.15.7
- Xcode 12.4
- Swift 5.3.2
2.2 搭建工程
首先创建一个名为“LineChart”的工程,并在 Storyboard 上放置一个 UICollectionView。创建一个新的 Swift 文件,命名为“LineChartView”。
2.3 实现 UICollectionViewDataSource 协议
在“LineChartView”文件中实现 UICollectionViewDataSource 协议,定义数据源相关方法的实现。示例代码如下:
class LineChartView: UIView, UICollectionViewDataSource {
// 数据源
var datas: [[CGFloat]] = []
// UICollectionView 相关方法
// 返回分区数
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
// 返回每个分区的行数
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return datas.count
}
// 返回 cell 样式
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "LineChartCell", for: indexPath) as! LineChartCell
cell.backgroundColor = .clear
let data = datas[indexPath.item]
cell.lineChartView.update(with: data)
return cell
}
}
2.4 实现 UICollectionViewDelegateFlowLayout 协议
实现 UICollectionViewDelegateFlowLayout 协议,定义单元格大小以及行间距。示例代码如下:
class LineChartView: UIView, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
// UICollectionViewDelegateFlowLayout 相关方法
// 返回 cell 的大小
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = bounds.width / CGFloat(datas.count)
let height = bounds.height - 2 * LineChartConstants.paddingY
return CGSize(width: width, height: height)
}
// 返回行间距
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
}
2.5 实现自定义单元格
创建一个名为“LineChartCell”的新文件,编辑代码以实现自定义单元格。示例代码如下:
class LineChartCell: UICollectionViewCell {
// 折线图视图
let lineChartView = LineChartCanvasView(frame: .zero)
// 初始化 cell
override init(frame: CGRect) {
super.init(frame: frame)
contentView.backgroundColor = .clear
contentView.addSubview(lineChartView)
lineChartView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
// 初始化失败
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
2.6 实现 LineChartConstants 结构
创建一个名为“LineChartConstants”的新文件,用于定义常量。示例代码如下:
struct LineChartConstants {
static let paddingX: CGFloat = 15 // X 轴边距
static let paddingY: CGFloat = 15 // Y 轴边距
static let gridLineColor: UIColor = UIColor(red: 0.85, green: 0.85, blue: 0.85, alpha: 1.0) // 网格线颜色
static let animationDuration: TimeInterval = 1.5 // 动画时长
}
2.7 实现 LineChartDataPoint 结构
创建一个名为“LineChartDataPoint”的新文件,用于定义数据点。示例代码如下:
struct LineChartDataPoint {
let x: CGFloat
let y: CGFloat
let title: String?
}
2.8 实现 LineChartCanvasView 类
创建一个名为“LineChartCanvasView”的新文件,绘制折线图。示例代码如下:
class LineChartCanvasView: UIView {
// 数据源
var dataPoints: [LineChartDataPoint] = []
// 被选中的数据点
var selectedDataPoint: LineChartDataPoint? {
didSet {
setNeedsDisplay()
}
}
// 绘制折线图
override func draw(_ rect: CGRect) {
super.draw(rect)
let context = UIGraphicsGetCurrentContext()!
context.clear(bounds)
drawGridLines(with: context)
drawAxes(with: context)
drawChartData(with: context)
drawSelectionIndicator(with: context)
}
// 绘制网格线
private func drawGridLines(with context: CGContext) {
let horizontalGridCount = 5
let verticalGridCount = 7
context.setLineWidth(1.0)
context.setStrokeColor(LineChartConstants.gridLineColor.cgColor)
let width = bounds.width - 2 * LineChartConstants.paddingX
let height = bounds.height - 2 * LineChartConstants.paddingY
let horizontalGridSpacing = height / CGFloat(horizontalGridCount)
let verticalGridSpacing = width / CGFloat(verticalGridCount - 1)
for i in 0...horizontalGridCount {
let y = CGFloat(i) * horizontalGridSpacing + LineChartConstants.paddingY
context.move(to: CGPoint(x: LineChartConstants.paddingX, y: y))
context.addLine(to: CGPoint(x: bounds.width - LineChartConstants.paddingX, y: y))
context.strokePath()
}
for i in 0..<verticalGridCount {
let x = CGFloat(i) * verticalGridSpacing + LineChartConstants.paddingX
context.move(to: CGPoint(x: x, y: LineChartConstants.paddingY))
context.addLine(to: CGPoint(x: x, y: bounds.height - LineChartConstants.paddingY))
context.strokePath()
}
}
// 绘制坐标轴
private func drawAxes(with context: CGContext) {
context.setLineWidth(2.0)
context.setStrokeColor(UIColor.black.cgColor)
context.move(to: CGPoint(x: LineChartConstants.paddingX, y: LineChartConstants.paddingY))
context.addLine(to: CGPoint(x: LineChartConstants.paddingX, y: bounds.height - LineChartConstants.paddingY))
context.move(to: CGPoint(x: LineChartConstants.paddingX, y: bounds.height - LineChartConstants.paddingY))
context.addLine(to: CGPoint(x: bounds.width - LineChartConstants.paddingX, y: bounds.height - LineChartConstants.paddingY))
context.strokePath()
}
// 绘制数据
private func drawChartData(with context: CGContext) {
if dataPoints.count < 2 {
return
}
let minDataPoint = dataPoints.min { a, b in a.y < b.y }!
let maxDataPoint = dataPoints.max { a, b in a.y < b.y }!
let dataRange = maxDataPoint.y - minDataPoint.y
let valueOffset = LineChartConstants.paddingY + dataRange / 5.0
let verticalScale = (bounds.height - 2 * LineChartConstants.paddingY) / (5.0 * dataRange)
let horizontalScale = (bounds.width - 2 * LineChartConstants.paddingX) / CGFloat(dataPoints.count - 1)
let path = UIBezierPath()
var lastPoint = CGPoint.zero
for (i, dataPoint) in dataPoints.enumerated() {
if i == 0 {
path.move(to: CGPoint(x: CGFloat(i) * horizontalScale + LineChartConstants.paddingX, y: bounds.height - (dataPoint.y - minDataPoint.y) * verticalScale - LineChartConstants.paddingY))
} else {
let point = CGPoint(x: CGFloat(i) * horizontalScale + LineChartConstants.paddingX, y: bounds.height - (dataPoint.y - minDataPoint.y) * verticalScale - LineChartConstants.paddingY)
let controlPoint1 = CGPoint(x: lastPoint.x + LineChartConstants.paddingX, y: lastPoint.y)
let controlPoint2 = CGPoint(x: point.x - LineChartConstants.paddingX, y: point.y)
path.addCurve(to: point, controlPoint1: controlPoint1, controlPoint2: controlPoint2)
}
lastPoint = CGPoint(x: CGFloat(i) * horizontalScale + LineChartConstants.paddingX, y: bounds.height - (dataPoint.y - minDataPoint.y) * verticalScale - LineChartConstants.paddingY)
}
context.setLineWidth(2.0)
context.setStrokeColor(UIColor(red: 0.1, green: 0.45, blue: 0.85, alpha: 1.0).cgColor)
context.addPath(path.cgPath)
context.strokePath()
for dataPoint in dataPoints {
let x = CGFloat(dataPoints.firstIndex(of: dataPoint)!) * horizontalScale + LineChartConstants.paddingX
let y = bounds.height - (dataPoint.y - minDataPoint.y) * verticalScale - LineChartConstants.paddingY
let rect = CGRect(x: x - 5, y: y - 5, width: 10, height: 10)
if let title = dataPoint.title {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
let attributes = [
NSAttributedString.Key.foregroundColor: UIColor.black,
NSAttributedString.Key.font: UIFont.systemFont(ofSize: 12.0),
NSAttributedString.Key.paragraphStyle: paragraphStyle
]
title.draw(with: CGRect(x: x - 40, y: y - 20, width: 80, height: 20), options: .usesLineFragmentOrigin, attributes: attributes, context: nil)
}
if dataPoint == selectedDataPoint {
context.setFillColor(UIColor.red.cgColor)
context.fillEllipse(in: rect.insetBy(dx: -3, dy: -3))
} else {
context.setFillColor(UIColor.white.cgColor)
context.fillEllipse(in: rect.insetBy(dx: -3, dy: -3))
context.setStrokeColor(UIColor.red.cgColor)
context.setLineWidth(2.0)
context.strokeEllipse(in: rect.insetBy(dx: -3, dy: -3))
}
}
}
// 绘制选中指示器
private func drawSelectionIndicator(with context: CGContext) {
guard let selectedDataPoint = selectedDataPoint else {
return
}
let x = CGFloat(dataPoints.firstIndex(of: selectedDataPoint)!) * (bounds.width - 2 * LineChartConstants.paddingX) / CGFloat(dataPoints.count - 1) + LineChartConstants.paddingX
let y = bounds.height - (selectedDataPoint.y - dataPoints.min{ $0.y < $1.y }!.y) / (dataPoints.max{ $0.y < $1.y }!.y - dataPoints.min{ $0.y < $1.y }!.y) * (bounds.height - 2 * LineChartConstants.paddingY) - LineChartConstants.paddingY
context.beginPath()
context.move(to: CGPoint(x: x, y: y - 5.0))
context.addLine(to: CGPoint(x: x - 5.0, y: y))
context.addLine(to: CGPoint(x: x, y: y + 5.0))
context.addLine(to: CGPoint(x: x + 5.0, y: y))
context.closePath()
context.setFillColor(UIColor.red.cgColor)
context.fillPath()
}
// 更新数据源
func update(with data: [CGFloat]) {
let dataPoints = data.enumerated().map { index, value in
LineChartDataPoint(x: CGFloat(index), y: value, title: "")
}
self.dataPoints = dataPoints
setNeedsDisplay()
}
}
2.9 实现 LineChartAnimator 类
创建一个名为“LineChartAnimator”的新文件,使用 CAShapeLayer 实现折线图动画。示例代码如下:
class LineChartAnimator {
func animate(_ layer: CAShapeLayer, duration: TimeInterval) {
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0.0
animation.toValue = 1.0
animation.duration = duration
layer.add(animation, forKey: "strokeEnd")
}
}
2.10 使用 LineChartView 组件
在 Storyboard 中,将 UICollectionView 的 Class 类型设置为“LineChartView”,并在 viewDidAppear 方法中添加以下代码:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let data1: [CGFloat] = [10, 20, 30, 40, 35, 45, 30]
let data2: [CGFloat] = [25, 35, 30, 15, 35, 20, 45]
chartView.datas = [data1, data2]
chartView.reloadData()
}
然后就可以在界面上看到多条折线图啦!
3. 结语
以上就是“iOS实现的多条折线图封装实例”的完整攻略。希望可以对你有所帮助。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:iOS实现的多条折线图封装实例 - Python技术站