iOS实现的多条折线图封装实例

下面是“iOS实现的多条折线图封装实例”的完整攻略。

1. 需求分析

在开始进行多条折线图封装前,我们需要明确需求,分析出我们所需要的功能和特性。

1.1 功能需求

  1. 绘制多条折线图;
  2. 支持同时显示多个数据源;
  3. 支持自定义颜色、线型、数据点形状等设置;
  4. 支持显示数据点的数值;
  5. 支持动画效果。

1.2 技术需求

  1. 使用 Core Graphics 绘制折线图;
  2. 使用 UIBezierPath 绘制曲线;
  3. 使用 CAShapeLayer 实现折线图动画效果;
  4. 使用 UICollectionView 实现数据源的展示;
  5. 使用 UICollectionViewFlowLayout 实现数据源的布局。

2. 技术实现

在满足需求的前提下,下面我们介绍如何实现这个多条折线图的封装。

2.1 开发环境

  1. macOS Catalina 10.15.7
  2. Xcode 12.4
  3. 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技术站

(0)
上一篇 2023年6月25日
下一篇 2023年6月25日

相关文章

  • 如何telnetipv6

    如何使用Telnet连接IPv6地址 Telnet是一种用于远程登录到计算机的协议,它可以通过网络连接到远程计算机并执行命令。在IPv6网络中,您可以使用Telnet连接IPv6地址。以下是使用TelnetIPv6地址的步骤: 1. 确定目标IPv6地址 首先,您需要确定要连接的IPv6地址。您可以使用ping命令或其他网络工具来确定目标IPv6地址。 2.…

    other 2023年5月6日
    00
  • bat脚本常用命令及亲测示例代码超详细讲解

    Bat脚本常用命令及亲测示例代码超详细讲解 1. 概述 Bat脚本,又称为批处理脚本,是Windows系统下的一种重要的命令行工具。通过编写Bat脚本,可以快速、批量地执行命令、调用程序、创建文件、删除文件等操作。本文将对Bat脚本中常用的一些命令进行详细讲解,并通过亲测示例代码让大家更好地理解和掌握。 2. 命令详解 2.1 echo echo命令用于在脚…

    other 2023年6月26日
    00
  • Android SlidingDrawer 抽屉效果的实现

    Android SlidingDrawer 抽屉效果的实现攻略 Android SlidingDrawer 是一个可以实现抽屉效果的控件,可以在屏幕上显示一个抽屉,用户可以通过滑动来打开或关闭抽屉。下面是一个详细的攻略,包含了实现抽屉效果的步骤和两个示例说明。 步骤 在 XML 布局文件中定义 SlidingDrawer 控件。例如: <Sliding…

    other 2023年8月25日
    00
  • MySQL数据库grant授权命令

    下面是 MySQL 数据库 grant 授权命令的完整攻略,包括授权命令的语法、使用方法和两个示例说明。 授权命令的语法 MySQL 数据库 grant 授权命令的语法如下: GRANT privileges ON database.table TO ‘user’@’host’ IDENTIFIED BY ‘password’; 其中,privileges …

    other 2023年5月5日
    00
  • win10纯净版exe应用程序打不开如何解决的图文步骤

    下面是关于 “win10纯净版exe应用程序打不开如何解决的图文步骤” 的详细攻略。 1. 问题描述 在使用 Win10 纯净版时,可能会遇到 exe 应用程序无法启动的问题。这可能是由于某些安全设置或其他因素导致的。那么应该如何解决这个问题呢? 2. 解决步骤 步骤一:检查 Windows 安全设置 打开 Windows 安全设置:在 Windows 搜索…

    other 2023年6月25日
    00
  • PHP实现递归无限级分类

    实现递归无限级分类是PHP中的常见问题,可以通过以下步骤进行解决: 步骤一:建立递归函数 首先建立递归函数,该函数能够实现对无限级分类进行递归处理。代码如下: function getTree($data, $pid = 0, $level = 0) { $tree = array(); foreach ($data as $row) { if ($row[…

    other 2023年6月27日
    00
  • Java Web开发防止多用户重复登录的完美解决方案

    Java Web开发防止多用户重复登录的完美解决方案 在 Java Web 开发中,通常需要考虑如何防止多用户重复登录的问题。为了避免这种情况的发生,我们可以采用以下方法来解决。 1. 使用 Session 实现用户登录控制 Session 是 Web 应用程序中的一种状态管理技术,用于在服务器端存储用户会话数据。通过使用 Session,我们可以轻松实现用…

    other 2023年6月26日
    00
  • Go并发编程中使用channel的方法

    下面我就来详细讲解Go并发编程中使用channel的方法的完整攻略。 什么是channel Go语言中的channel是一种通信机制,用于协调多个goroutine之间的交互和同步。简单来说,channel就是一个通道,通过它可以在goroutine之间传递数据,实现数据共享,实现同步或异步的通信。 channel的创建和关闭 channel是通过内置函数m…

    other 2023年6月27日
    00
合作推广
合作推广
分享本页
返回顶部