导读:本期聚焦于深圳程序员创作的《如何用Core Graphics实现iOS端折线图:数据点连接、贝塞尔曲线平滑与触摸交点高亮?》,敬请观看详情。为什么你的iOS图表库画出来的折线图总感觉不够流畅,触摸时又找不到准确的落点?其实用系统自带的Core Graphics就能做出既平滑又交互精准的折线图。本文将从零开始讲解绘制流程:先把原始数据映射成屏幕坐标点,再用直线逐段连接形成基础折线,然后介绍两种常见的贝塞尔曲线平滑方案,分析三次曲线控制点的计算思路与各自的效果差异,最后实现手指滑动时通过最近点查找算法定位交点并绘制高亮圆点与提示信息。全文附有完整Swift代码示例,帮助你脱离第三方图表库,掌握图表绘制的核心原理。

在iOS开发中,展示数据趋势最常见的方式就是折线图。虽然市面上有Charts等第三方库,但依赖第三方库往往带来包体积膨胀、定制困难等问题。其实Core Graphics配合UIKit的绘制机制,完全可以实现一套轻量、可控的折线图控件。本文围绕数据点坐标映射、贝塞尔曲线平滑处理以及触摸交点高亮三个核心环节,详细讲解实现思路和代码。

如何用Core Graphics实现iOS端折线图:数据点连接、贝塞尔曲线平滑与触摸交点高亮?

一、数据模型与坐标映射:把业务数据变成屏幕坐标

绘制折线图的第一步是定义数据模型。最简单的做法是用一个结构体数组表示每个数据点,包含业务值和可选的横轴标签。业务数据的取值范围和视图尺寸完全是两个坐标系,必须先做归一化映射,否则画出来的图形会跑出视图边界。

struct ChartPoint {
    let label: String
    let value: Double
}

struct LineChartRenderer {
    var points: [ChartPoint] = []
    var minValue: Double = 0
    var maxValue: Double = 0

    // 把业务值转换为视图坐标系中的y值
    // 注意:UIKit默认坐标系y轴向下,需要翻转
    func screenY(for value: Double, in rect: CGRect) -> CGFloat {
        let padding: CGFloat = 20
        guard maxValue > minValue else { return rect.midY }
        let ratio = (value - minValue) / (maxValue - minValue)
        return rect.height - padding - CGFloat(ratio) * (rect.height - padding * 2)
    }

    func screenX(at index: Int, count: Int, in rect: CGRect) -> CGFloat {
        guard count > 1 else { return rect.midX }
        let usable = rect.width - 40
        return 20 + CGFloat(index) / CGFloat(count - 1) * usable
    }
}

这里有一个容易踩的坑:Core Graphics原始坐标系的y轴是向上的,而UIKit对UIView的绘制上下文做了翻转,默认y轴向下。上面的代码在计算y值时用总高度减去归一化结果,就是为了适配这种翻转关系。如果不做处理,画出来的图形会上下颠倒,初学者十有八九会在这里卡住。

横坐标的分配策略也值得斟酌。如果数据点是等间隔的(比如每小时一个采样点),按索引均分即可;如果是时间序列且间隔不均匀,就应该按真实时间差计算横坐标,否则曲线的走势会被压扁或拉伸,误导使用者对趋势的判断。

二、直线连接与贝塞尔曲线平滑的两种方案

基础折线图的绘制非常直观:遍历所有映射后的坐标点,调用addLine逐段连接,最后stroke出路径。但直线连接在数据点较多时会显得生硬,视觉上缺乏流畅感。这时就需要用贝塞尔曲线对折线做平滑处理。

第一种是二次曲线(QuadCurve)方案。它的原理是以上一个数据点作为控制点,先画到相邻两点的中点,再以中点作为过渡继续绘制,最终曲线一定经过每个数据点。实现代码如下:

func quadPath(points: [CGPoint]) -> UIBezierPath {
    let path = UIBezierPath()
    guard points.count > 1 else { return path }
    path.move(to: points[0])
    for i in 1..<points.count {
        let prev = points[i - 1]
        let current = points[i]
        let mid = CGPoint(x: (prev.x + current.x) / 2,
                          y: (prev.y + current.y) / 2)
        path.addQuadCurve(to: mid, control: prev)
    }
    path.addLine(to: points[points.count - 1])
    return path
}

QuadCurve的优点是计算量小、实现简单;缺点是曲率变化不够自然,在数据剧烈波动处会出现过渡生硬的问题。更高级的做法是使用三次贝塞尔曲线,为每一段曲线分别计算起点控制点和终点控制点。常见的计算方式是让控制点沿着前后数据点连线的方向偏移,偏移系数通常取0.3到0.5之间。系数越大曲线越圆润,但也越容易产生过冲,也就是曲线的波峰超过实际数据点的极值,造成数据失真。折中的办法是在极值点处强制控制点保持水平,保证曲线不会越过真实的峰值。

// 计算三次贝塞尔曲线的控制点,smoothness为平滑系数,建议取0.3
func controlPoints(p0: CGPoint, p1: CGPoint, p2: CGPoint,
                   p3: CGPoint, smoothness: CGFloat) -> (CGPoint, CGPoint) {
    let center = CGPoint(x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2)
    let dx = center.x - (p0.x + p3.x) / 2
    let dy = center.y - (p0.y + p3.y) / 2
    var c1 = CGPoint(x: p1.x + (p2.x - p0.x) * smoothness,
                     y: p1.y + dy * smoothness)
    var c2 = CGPoint(x: p2.x - (p3.x - p1.x) * smoothness,
                     y: p2.y - dy * smoothness)
    // 极值点处强制水平,防止曲线过冲越过真实峰值
    if (p1.y < p0.y && p1.y < p2.y) || (p1.y > p0.y && p1.y > p2.y) {
        c1.y = p1.y
    }
    if (p2.y < p1.y && p2.y < p3.y) || (p2.y > p1.y && p2.y > p3.y) {
        c2.y = p2.y
    }
    return (c1, c2)
}

两种方案怎么选?如果只是做简单的趋势展示,QuadCurve完全够用且性能更好;如果是金融类、监控类的图表,对数据准确性要求高,建议使用带极值保护的三次曲线方案,或者干脆提供一个可配置的平滑开关,让调用方按场景自行决定。

三、触摸交点高亮:最近点查找与命中判定

交互是折线图的灵魂。用户手指在图表上滑动时,需要实时找到距离手指最近的曲线点,并绘制高亮圆点和数值提示。实现思路是重写touchesBegan、touchesMoved和touchesEnded,在其中把触摸坐标与所有数据点做距离比较。

var highlightIndex: Int?

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard let touch = touches.first else { return }
    let location = touch.location(in: self)
    // 只比较横向距离,避免曲线陡峭时命中困难
    var nearest = 0
    var minDistance = CGFloat.greatestFiniteMagnitude
    for (index, point) in screenPoints.enumerated() {
        let d = abs(point.x - location.x)
        if d < minDistance {
            minDistance = d
            nearest = index
        }
    }
    highlightIndex = nearest
    setNeedsDisplay()
}

查找算法的细节决定了手感。如果用欧氏距离(同时比较x和y),手指在曲线下方滑动时可能命中错误的数据点,因为陡峭段的y差值会主导距离计算。只比较横向距离是更符合直觉的做法:用户感知的是手指对齐了哪个数据列,而不是精确贴在曲线上。对于数据点极多的高频场景,线性查找可以优化为二分查找,因为数据点的x坐标天然有序。

命中之后的高亮绘制建议放在draw(_ rect:)里统一处理,而不是额外叠加一个子视图。高亮层通常包含三部分:曲线上的实心圆点、圆点外圈的半透明光晕、以及顶部的数值气泡。气泡可以用UIBezierPath画一个带小三角的圆角矩形,文字部分用NSAttributedString的draw方法渲染。手指抬起时可以延迟几百毫秒再清除高亮,避免快速滑动时高亮反复闪烁。

四、性能优化与绘制细节

如果图表需要频繁刷新(比如实时数据流),务必把坐标映射的结果缓存起来,只在数据变化时重新计算,draw方法内只做纯绘制操作,避免每次重绘都重复遍历数据做浮点运算。

另一个提升滚动性能的思路是把网格线、渐变填充、曲线拆分到不同的CAShapeLayer或独立视图,图层内容不变时不会触发重绘。数据量达到数千点时,还可以做抽稀处理,保留极值点、丢弃中间的平稳点,肉眼几乎看不出差异,但绘制耗时能下降一个数量级。

最后,绘制曲线时注意线帽和拐角设置:setLineCap(.round)和setLineJoin(.round)能让曲线端点和转折处更圆润;如果要给曲线下方加渐变填充,记得用曲线路径闭合到底边后再clip填充区域,并使用saveGState与restoreGState保护图形状态,避免污染后续绘制。掌握这些细节后,一个不依赖任何第三方库的折线图控件就成型了。

Core Graphics折线图贝塞尔曲线修改时间:2026-09-09 20:17:07

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0909/53584.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。