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
相关产品推荐
相关产品推荐

