You need to enable JavaScript to run this app. SwiftUI 如何将自定义折线图的尖锐边角改为圆角
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI 如何将自定义折线图的尖锐边角改为圆角

SwiftUI 折线图折点圆角实现方案

常见尝试失效的原因

  • .cornerRadius(5) 修饰符只会对Shape渲染完成后的整体外轮廓做圆角裁剪,作用范围是整个Path的最外层边界,完全不会干预折线上相邻线段衔接形成的尖夹角,因此对折点圆角没有任何效果
  • 之前注释掉的addQuadCurve调用位置完全偏离逻辑:没有嵌入到相邻数据点的连线流程中,单独在路径起点和右下角位置画一条曲线,本质是在主折线路径外追加了一条无关的路径片段,完全不会影响折点本身的尖锐感
  • 折点圆角的正确实现逻辑,是在每两个相邻数据点的衔接位置,用二次贝塞尔曲线替代硬直连线,通过控制两侧曲线的过渡点位置,把原本尖锐的夹角平滑成圆弧效果

可直接复用的修改后代码

把原来的硬连线Path替换为带圆角过渡的逻辑,同时支持渐变填充区域和线条样式共用同一套圆角逻辑:

import SwiftUI

@available(iOS 14.0, *)
struct Graph: View {
    var styling = ViewStyling()
    var stockPrices: [CGFloat]
    // 可调的折线折点圆角半径
    var lineCornerRadius: CGFloat = 4

    var body: some View {
        ZStack {
            LinearGradient(
                gradient: Gradient(colors: [styling.gradientColor, styling.defaultWhite]),
                startPoint: .top,
                endPoint: .bottom
            )
            .clipShape(
                LineGraph(
                    dataPoints: stockPrices.normalized,
                    closed: true,
                    cornerRadius: lineCornerRadius
                )
            )

            LineGraph(
                dataPoints: stockPrices.normalized,
                cornerRadius: lineCornerRadius
            )
            .stroke(styling.graphLine, lineWidth: 2)
        }
    }
}

@available(iOS 14.0, *)
struct LineGraph: Shape {
    var dataPoints: [CGFloat]
    var closed = false
    // 折点圆角半径,过大会导致线条扭曲
    var cornerRadius: CGFloat = 4

    func path(in rect: CGRect) -> Path {
        // 根据索引计算对应数据点的画布坐标
        func point(at ix: Int) -> CGPoint {
            let pointValue = dataPoints[ix]
            let x = rect.width * CGFloat(ix) / CGFloat(dataPoints.count - 1)
            let y = (1 - pointValue) * rect.height
            return CGPoint(x: x, y: y)
        }

        return Path { p in
            guard dataPoints.count > 1 else { return }
            // 提前把所有数据点转换为画布上的实际坐标
            let points = (0..<dataPoints.count).map { point(at: $0) }
            // 从第一个点开始绘制路径
            p.move(to: points[0])

            // 逐段遍历相邻点,插入圆角过渡
            for i in 1..<points.count {
                let currentPoint = points[i]
                let previousPoint = points[i-1]
                // 计算当前线段的长度,限制最大圆角半径,防止圆角超过线段长度导致变形
                let segmentLength = hypot(
                    currentPoint.x - previousPoint.x,
                    currentPoint.y - previousPoint.y
                )
                let validRadius = min(cornerRadius, segmentLength / 2)

                // 计算当前线段上、距离当前点长度为validRadius的过渡锚点
                let prevSegmentMid = CGPoint(
                    x: (previousPoint.x + currentPoint.x) / 2,
                    y: (previousPoint.y + currentPoint.y) / 2
                )
                let anchorPoint = CGPoint(
                    x: prevSegmentMid.x + (previousPoint.x - prevSegmentMid.x) * (validRadius / (segmentLength / 2)),
                    y: prevSegmentMid.y + (previousPoint.y - prevSegmentMid.y) * (validRadius / (segmentLength / 2))
                )

                // 非最后一个点时,计算下一段线段的过渡锚点,用二次贝塞尔衔接两个锚点
                if i < points.count - 1 {
                    let nextPoint = points[i+1]
                    let nextSegmentLength = hypot(
                        nextPoint.x - currentPoint.x,
                        nextPoint.y - currentPoint.y
                    )
                    let nextValidRadius = min(cornerRadius, nextSegmentLength / 2)
                    let nextSegmentMid = CGPoint(
                        x: (currentPoint.x + nextPoint.x) / 2,
                        y: (currentPoint.y + nextPoint.y) / 2
                    )
                    let nextAnchor = CGPoint(
                        x: nextSegmentMid.x + (nextPoint.x - nextSegmentMid.x) * (nextValidRadius / (nextSegmentLength / 2)),
                        y: nextSegmentMid.y + (nextPoint.y - nextSegmentMid.y) * (nextValidRadius / (nextSegmentLength / 2))
                    )
                    // 控制点设为原本的折点,实现平滑圆弧过渡
                    p.addQuadCurve(to: nextAnchor, control: currentPoint)
                } else {
                    // 最后一个点直接连线收尾
                    p.addLine(to: currentPoint)
                }
            }

            // 填充区域路径:闭合到图表底部
            if closed {
                p.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
                p.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
                p.closeSubpath()
            }
        }
    }
}

extension Array where Element == CGFloat {
    // 将数值数组归一化到0~1区间,适配图表坐标映射
    var normalized: [CGFloat] {
        if let min = self.min(), let max = self.max() {
            return self.map{ ($0 - min) / (max - min) }
        }
        return []
    }
}

实现注意事项

  • 圆角半径建议控制在3~6之间,匹配2pt线宽的视觉比例,数值过大会导致过渡点重叠,出现线条扭曲
  • 这套Shape实现完全兼容小组件的渲染环境,不需要额外做坐标适配
  • 归一化逻辑保留了原代码的数值映射规则,不会改变原有折线的展示比例

内容的提问来源于stack exchange,提问作者Nicolaj Spiegelhauer

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.30 16:06:02