iOS - 折线图 Swift 实现
背景:由于公司项目需要绘制曲线图,目前只有用到折线图这一种,一开始并没有自己绘制折线图的想法,只是找了一个第三方的库来用,图一个简单方便。但是找了很久,找到一个勉强能够使用,但是效率太差,数据量大的时候效率非常的差,并不能在修改数据的时候时时的显示出最新的绘制效果。
基本需求:
- 实现折线图
- 连接点大小可自定义
- 可以绘制多条折线
- 可以给不同折线标记颜色
- 可以添加图例
- 实现 X 轴,Y 轴
代码如下:
// // SPlotView.swift //// // Created by mac min on 2021/1/29. // Copyright © 2021 Inledco. All rights reserved. // import UIKit class PlotView: UIView { // 顶部边距 var topMargin: CGFloat = 5.0 // 左边距 var leftMargin: CGFloat = 40.0 // 底部边距 var bottomMargin: CGFloat = 30.0 // 右边距 var rightMargin: CGFloat = 20.0 // X轴最小值 var xMinValue: CGFloat = 0.0 // X轴最大值 var xMaxValue: CGFloat = 100.0 // Y轴最小值 var yMinValue: CGFloat = 0.0 // Y轴最大值 var yMaxValue: CGFloat = 100.0 // Y轴间隔 var yInterval: CGFloat = 25.0 // X间隔 var xInterval: CGFloat = 120.0 // 是否显示Y轴 var yAxisEnable: Bool = false // 曲线图宽度:除去边距 var plotWidth: CGFloat = 0.0 // 曲线图高度:除去边距 var plotHeight: CGFloat = 0.0 // 标记是否已经添加横坐标 纵坐标 图例等 var isAddLabel: Bool = false /** * 数据点 * 格式: * 1. 一层数组包含每种颜色的数据 * 2. 数组中的每个对象包含对应颜色的所有数据 */ var dataPointArray: [[CGPoint]]? // 线条颜色 var lineColorArray: [UIColor]? // 线条颜色名称 var lineColorTitleArray: [String]? // 预览指示器 var indicatorLabel: UILabel = UILabel.init(frame: CGRect.zero) override init(frame: CGRect) { super.init(frame: frame) self.translatesAutoresizingMaskIntoConstraints = false self.backgroundColor = .clear self.plotWidth = frame.size.width - self.leftMargin - self.rightMargin self.plotHeight = frame.size.height - self.topMargin - self.bottomMargin self.isAddLabel = false self.indicatorLabel.frame = CGRect.init(x: 0.0, y: self.topMargin, width: 1.0, height: self.plotHeight) self.indicatorLabel.isHidden = true self.indicatorLabel.backgroundColor = .green self.addSubview(self.indicatorLabel) } required init?(coder: NSCoder) { super.init(coder: coder) } // 添加图例 func addLegend(colorArray: [UIColor], colorTitleArray: [String]) -> Void { var colorLableWidth = self.leftMargin var previousTitleLabel: UILabel? for i in 0..<colorArray.count { if i != 0 { colorLableWidth = colorLableWidth + 20 } let label = UILabel.init(frame: CGRect(x: colorLableWidth, y: self.frame.size.height - 10, width: 10, height: 10)) label.translatesAutoresizingMaskIntoConstraints = false label.backgroundColor = colorArray[i] self.addSubview(label) // 添加约束 var labelLeadingLayoutConstraint = NSLayoutConstraint(item: label, attribute: .leading, relatedBy: .equal, toItem: self, attribute: .leading, multiplier: 1.0, constant: self.leftMargin) let labelBottomLayoutConstraint = NSLayoutConstraint(item: label, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: -8.0) let labelHeightLayoutConstraint = NSLayoutConstraint(item: label, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 10.0) let labelWidthLayoutConstraint = NSLayoutConstraint(item: label, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 10.0) if previousTitleLabel != nil { labelLeadingLayoutConstraint = NSLayoutConstraint(item: label, attribute: .leading, relatedBy: .equal, toItem: previousTitleLabel, attribute: .trailing, multiplier: 1.0, constant: 8.0) } self.addConstraints([labelLeadingLayoutConstraint, labelBottomLayoutConstraint, labelHeightLayoutConstraint, labelWidthLayoutConstraint]) let titleLabel = UILabel(frame: CGRect(x: 15 + colorLableWidth, y: self.frame.size.height - 10, width: SystemInfo.screenWidth / CGFloat((colorArray.count + 1)), height: 10.0)) titleLabel.translatesAutoresizingMaskIntoConstraints = false titleLabel.font = UIFont.ex_systemFontOfSize(fontSize: 8.0) titleLabel.text = colorTitleArray[i] titleLabel.textColor = .white self.addSubview(titleLabel) // 添加约束 let titleLabelLeadingLayoutConstraint = NSLayoutConstraint(item: titleLabel, attribute: .leading, relatedBy: .equal, toItem: label, attribute: .trailing, multiplier: 1.0, constant: 4.0) let titleLabelBottomLayoutConstraint = NSLayoutConstraint(item: titleLabel, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: -8.0) self.addConstraints([titleLabelLeadingLayoutConstraint, titleLabelBottomLayoutConstraint]) previousTitleLabel = titleLabel } } // 刷新视图 func refreshPlotView() -> Void { self.plotWidth = self.frame.size.width - self.leftMargin - self.rightMargin self.plotHeight = self.frame.size.height - self.topMargin - self.bottomMargin // KMYLOG(@"self.plotHeight = %f", self.frame.size.height - self.topMargin - self.bottomMargin); // 是否添加横纵坐标 if (self.isAddLabel == false && self.plotHeight > 0) { self.addXLabel() self.addYLabel() self.isAddLabel = true } self.setNeedsDisplay() } // MARK: 添加 X 轴刻度 private func addXLabel() -> Void { let xUint = self.plotWidth / (self.xMaxValue - self.xMinValue) for i in 0..<GlobalConstant.DAY_SEPARATE_BY_MINUTE / Int(self.xInterval) + 1 { if i % 3 == 0 { let label = UILabel.init(frame: CGRect.init(x: self.leftMargin + CGFloat(i) * xUint * self.xInterval - 10, y: self.frame.size.height - 38, width: self.frame.size.width / 5, height: 20)) label.text = String.init(format: "%02d:00", i * 2) if i == GlobalConstant.DAY_SEPARATE_BY_MINUTE / Int(self.xInterval) { label.text = "00:00" } label.textColor = .white label.textAlignment = .left self.addSubview(label) } } } // MARK: 添加 Y 轴刻度 private func addYLabel() -> Void { let yUint = self.plotHeight / (self.yMaxValue - self.yMinValue) // 线条数 let labelCount: Int = Int((self.yMaxValue - self.yMinValue) / self.yInterval + 1) for i in 0..<labelCount { let label = UILabel.init(frame: CGRect(x: 0, y: yUint * CGFloat(i) * self.yInterval + self.topMargin - 8.0, width: self.leftMargin, height: 16.0)) label.text = String(format: "%2d", 100 - i * 25) label.textColor = .white label.textAlignment = .center self.addSubview(label) } } // Only override draw() if you perform custom drawing. // An empty implementation adversely affects performance during animation. override func draw(_ rect: CGRect) { if self.dataPointArray == nil { return } // 绘制坐标图 let context = UIGraphicsGetCurrentContext() context?.setStrokeColor(UIColor.white.cgColor) // X轴与Y轴单位长度 let xUint = self.plotWidth / self.xMaxValue let yUint = self.plotHeight / self.yMaxValue // 绘制Y轴 if self.yAxisEnable == true { context?.beginPath() context?.move(to: CGPoint(x: self.leftMargin, y: self.topMargin)) context?.addLine(to: CGPoint(x: self.leftMargin, y: self.plotHeight + self.topMargin)) context?.closePath() context?.strokePath() } // 绘制X轴刻度 for i in 0..<GlobalConstant.DAY_SEPARATE_BY_MINUTE / Int(self.xInterval) + 1 { context?.beginPath() context?.move(to: CGPoint(x: self.leftMargin + CGFloat(i) * xUint * self.xInterval, y: self.plotHeight + self.topMargin - 10)) context?.addLine(to: CGPoint(x: self.leftMargin + CGFloat(i) * xUint * self.xInterval, y: self.plotHeight + self.topMargin)) context?.closePath() context?.strokePath() } // 绘制X轴及横线 let horizonalCount: Int = Int(self.yMaxValue / self.yInterval + 1) for i in 0..<horizonalCount { context?.beginPath() context?.move(to: CGPoint(x: self.leftMargin, y: self.plotHeight / 4.0 * CGFloat(i) + self.topMargin)) context?.addLine(to: CGPoint(x: self.plotWidth + self.leftMargin, y: self.plotHeight / 4.0 * CGFloat(i) + self.topMargin)) context?.closePath() context?.strokePath() } // 绘制曲线 context?.setLineWidth(1.5) context?.beginPath() for i in 0..<self.dataPointArray!.count { if self.lineColorArray == nil || i > self.lineColorArray!.count - 1 { context?.setStrokeColor(UIColor.white.cgColor) context?.setFillColor(UIColor.white.cgColor) } else { context?.setStrokeColor(self.lineColorArray![i].cgColor) context?.setFillColor(self.lineColorArray![i].cgColor) } let lineDataArray = self.dataPointArray![i] if lineDataArray.count < 1 { return } for j in 0..<lineDataArray.count - 1 { let prePoint: CGPoint = lineDataArray[j] let nextPoint: CGPoint = lineDataArray[j + 1] let preX: CGFloat = prePoint.x * xUint + self.leftMargin let preY: CGFloat = self.plotHeight - prePoint.y * yUint + self.topMargin let nextX: CGFloat = nextPoint.x * xUint + self.leftMargin let nextY: CGFloat = self.plotHeight - nextPoint.y * yUint + self.topMargin context?.move(to: CGPoint(x: preX, y: preY)) context?.addLine(to: CGPoint(x: nextX, y: nextY)) context?.strokePath() context?.addArc(center: CGPoint(x: preX, y: preY), radius: 3.0, startAngle: 0.0, endAngle: CGFloat.pi * 2, clockwise: true) if j == lineDataArray.count - 2 { context?.addArc(center: CGPoint(x: nextX, y: nextY), radius: 3.0, startAngle: 0.0, endAngle: CGFloat.pi, clockwise: true) } context?.fillPath() } } } override func layoutSubviews() { super.layoutSubviews() // 使用约束时刷新曲线图 self.refreshPlotView() } }

自律 平静 思考 实践
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 因为Apifox不支持离线,我果断选择了Apipost!
· 通过 API 将Deepseek响应流式内容输出到前端