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

yizhihongxing

下面是“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日

相关文章

  • C语言深入分析数组指针和指针数组的应用

    C语言深入分析数组指针和指针数组的应用 数组指针和指针数组是C语言中比较重要的概念。数组指针是指一个指向数组的指针,而指针数组是指一个数组,其中的每个元素都是一个指针。以下将详细讲解这两个概念的应用。 数组指针的应用 声明和初始化 数组指针可以用来访问多维数组中的元素。对于一个二维数组,可以使用数组指针进行访问、初始化和赋值。例如: int arr[2][3…

    other 2023年6月25日
    00
  • 聊聊关于Java方法重写的反思

    下面我会详细讲解一下“聊聊关于Java方法重写的反思”的完整攻略。 什么是Java方法重写? Java方法重写是指子类中的方法覆盖了父类中相同方法名称、方法参数列表(包括参数类型和参数顺序)、方法返回值类型及访问修饰符的方法。 Java方法重写的优点 Java方法重写的优点主要有以下两点: 提高代码的复用性:当子类继承父类时,可以重写父类的某些方法,从而满足…

    other 2023年6月27日
    00
  • Resource Hacker 汉化版图文使用教程

    Resource Hacker 汉化版图文使用教程 Resource Hacker 是一款功能强大的 Windows 资源编辑器,可用于修改并编辑 exe、dll、ocx 等系统文件。在本教程中,我们将介绍如何使用 Resource Hacker 进行汉化操作。 步骤一:下载和安装 Resource Hacker 下载 Resource Hacker 汉化版…

    other 2023年6月26日
    00
  • JavaScript中constructor()方法的使用简介

    JavaScript中constructor()方法的使用简介 1. constructor()方法的概述 在JavaScript中,每个对象都有一个constructor方法,该方法返回创建该对象的构造函数。constructor方法通常用于检测对象类型。 2. 使用constructor()方法检测对象类型 可以使用constructor方法来检测对象的…

    other 2023年6月26日
    00
  • iOS/iPadOS 14.6 开发者预览版 Beta 2正式更新

    iOS/iPadOS 14.6 开发者预览版 Beta 2 正式更新,是苹果公司针对 iOS 和 iPadOS 开发者推出的操作系统预览版,供其进行应用程序和设备兼容测试,并在正式版本发布前提供调试和优化。 以下是详细的操作步骤: 准备工作 确保你的设备是支持 iOS/iPadOS 14.6 开发者预览版 Beta 2 更新的,可前往苹果官网查看支持列表。 …

    other 2023年6月26日
    00
  • 苹果推送(APNs)ios push小结

    苹果推送(APNs)ios push小结 简介 iOS推送通知是一种重要的功能,它可以让App在后台时获得用户的消息提醒,提高用户体验。iOS推送通知的实现依赖苹果推送服务(APNs)。APNs是一种基于HTTP/2协议的推送服务,通过APNs,开发者可以将消息和声音等推送给用户,以供App在后台时获得用户的消息提醒。 基本架构 APNs的基本架构如下: A…

    其他 2023年3月28日
    00
  • Win7开机无信号无法正常登陆怎么办?Win7开机无信号无法正常登陆的两种解决方法

    Win7开机无信号无法正常登陆是一个常见的问题,可能由于硬件问题或软件问题导致。以下是两种解决此问题的方法: 方法一: 安全模式启动 在Windows 7中,安全模式用于修复系统故障,避免操作系统无法启动。使用安全模式启动计算机后,可以卸载非法软件和文件,扫描硬盘将出错的文件重新安装,或还原系统设置。 以下是安全模式启动步骤: 关闭电脑 按下电源开关后,立即…

    other 2023年6月27日
    00
  • Spring配置文件的超详细图文介绍

    让我来给你详细讲解关于“Spring配置文件的超详细图文介绍”的完整攻略。 什么是Spring配置文件? Spring配置文件是指对Spring应用程序进行配置的XML文件,其中包含了Spring中的一些核心概念,比如Bean、AOP、事务等等模块的配置信息。通过配置文件,Spring框架能够根据应用程序的需求来创建和管理实例对象,提高开发效率和代码的可维护…

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