# 好看的图表怎么画，看完这几个 API 你就会了

·  阅读 3841

「这是我参与11月更文挑战的第22天，活动详情查看：2021最后一次更文挑战

## 先来一波概念

1. 绘制一个带圆角的矩形
``````RoundedRectangle(cornerRadius: 4)

1. 用颜色或渐变填充此形状。
``````public func fill<S>(_ content: S, style: FillStyle = FillStyle()) -> some View where S : ShapeStyle

1. 按给定的尺寸和锚点，对视图进行缩放
``````public func scaleEffect(_ scale: CGSize, anchor: UnitPoint = .center) -> some View

1. 动画
``````public func animation(_ animation: Animation?) -> some View

1. 阴影
``````public func shadow(color: Color = Color(.sRGBLinear, white: 0, opacity: 0.33), radius: CGFloat, x: CGFloat = 0, y: CGFloat = 0) -> some View

1. 创建一个路径
``````var path = Path()

1. 在指定点开始一个新的子路径
``````public mutating func move(to p: CGPoint)

1. 将二次贝塞尔曲线添加到路径中，并具有指定的端点和控制点
``````public mutating func addQuadCurve(to p: CGPoint, control cp: CGPoint)

1. 将圆弧添加到路径中，指定半径和角度
``````public mutating func addArc(center: CGPoint, radius: CGFloat, startAngle: Angle, endAngle: Angle, clockwise: Bool, transform: CGAffineTransform = .identity)

1. 从当前点到指定点追加一条直线段
``````public mutating func addLine(to p: CGPoint)

1. 关闭并完成当前子路径
``````public mutating func closeSubpath()

1. 使用颜色或渐变描绘此形状的轮廓
``````public func stroke<S>(_ content: S, style: StrokeStyle) -> some View where S : ShapeStyle

1. 围绕指定点旋转此视图的渲染输出
``````public func rotationEffect(_ angle: Angle, anchor: UnitPoint = .center) -> some View

1. 围绕给定的旋转轴在三个维度上旋转此视图
``````public func rotation3DEffect(_ angle: Angle, axis: (x: CGFloat, y: CGFloat, z: CGFloat), anchor: UnitPoint = .center, anchorZ: CGFloat = 0, perspective: CGFloat = 1) -> some View

1. 在视图上添加手势
``````public func gesture<T>(_ gesture: T, including mask: GestureMask = .all) -> some View where T : Gesture

## 代码实践

### 柱状图

``````@frozen public struct RoundedRectangle : Shape {

public var cornerSize: CGSize

public var style: RoundedCornerStyle

@inlinable public init(cornerSize: CGSize, style: RoundedCornerStyle = .circular)

@inlinable public init(cornerRadius: CGFloat, style: RoundedCornerStyle = .circular)

/// Describes this shape as a path within a rectangular frame of reference.
///
/// - Parameter rect: The frame of reference for describing this shape.
///
/// - Returns: A path that describes this shape.
public func path(in rect: CGRect) -> Path

/// The data to animate.
public var animatableData: CGSize.AnimatableData

/// The type defining the data to animate.
public typealias AnimatableData = CGSize.AnimatableData

/// The type of view representing the body of this view.
///
/// When you create a custom view, Swift infers this type from your
/// implementation of the required ``View/body-swift.property`` property.
public typealias Body
}

``````RoundedRectangle(cornerRadius: 4)

``````GeometryReader { geometry in
HStack(alignment: .bottom, spacing: getSpaceWidth(width: geometry.frame(in: .local).width - 20)) {
ForEach(0..<self.data.count, id: \.self){ i in
.....
}
}

``````@inlinable public func scaleEffect(_ scale: CGSize, anchor: UnitPoint = .center) -> some View

``````RoundedRectangle(cornerRadius: 4)
.frame(width: CGFloat(self.cellWidth))
.scaleEffect(CGSize(width: 1, height: self.scaleValue), anchor: .bottom)
.onAppear {
self.scaleValue = self.value
}
.animation(.spring().delay(self.touchLocation < 0 ? Double(self.index) * 0.04 : 0))

``````RoundedRectangle(cornerRadius: 4)
.frame(width: CGFloat(self.cellWidth))
.scaleEffect(CGSize(width: 1, height: self.scaleValue), anchor: .bottom)
.onAppear {
self.scaleValue = self.value
}
.animation(.spring().delay(self.touchLocation < 0 ? Double(self.index) * 0.04 : 0))

``````.gesture(DragGesture().onChanged({ value in
self.touchLocation = value.location.x / self.formSize.width
self.showValue = true
self.currentValue = self.getCurrentValue()?.1 ?? 0
if self.data.valuesGiven && self.formSize == ChartForm.medium {
self.showLabelValue = true
}
}).onEnded({ value in
self.showValue = false
self.showLabelValue = false
self.touchLocation = -1
})
)
.gesture(TapGesture())

### 饼状图

``````var path: Path {
var path = Path()
path.addArc(center: CGPoint(x: 200, y: 200), radius: 100, startAngle: Angle(degrees: 0), endAngle: Angle(degrees: 360), clockwise: false)
return path
}

1. Path 是 SwiftUI 提供的一个用于绘制 2D 图形的结构体，我称之为路径。

``````public mutating func addArc(center: CGPoint, radius: CGFloat, startAngle: Angle, endAngle: Angle, clockwise: Bool, transform: CGAffineTransform = .identity)

``````public mutating func addLine(to p: CGPoint)

1. closeSubpath 函数的定义为：
``````public mutating func closeSubpath()

``````var body: some View {
ZStack {
ForEach(0..<self.slices.count) { i in
PieChartCell(rect: geometry.frame(in: .local), startDeg: self.slices[i].startDeg, endDeg: self.slices[i].endDeg, index: i, backgroundColor: self.backgroundColor, accentColor: self.accentColor)
}
}
}
}

``````var slices: [PieSlice] {
var tempSlices:[PieSlice] = []
var lastEndDeg: Double = 0
let maxValue = data.reduce(0, +)
for slice in data {
let normalized: Double = Double(slice) / Double(maxValue)
let startDeg = lastEndDeg
let endDeg = lastEndDeg + (normalized * 360)
lastEndDeg = endDeg
tempSlices.append(PieSlice(startDeg: startDeg, endDeg: endDeg, value: slice, normalizedValue: normalized))
}
return tempSlices
}

``````struct PieChartCell: View {
@State private var show: Bool = false
var rect: CGRect
return min(rect.width, rect.height) / 2
}
var startDeg: Double
var endDeg: Double
var path: Path {
var path = Path()
path.closeSubpath()
return path
}

var index: Int
var backgroundColor: Color
var accentColor: Color

var body: some View {
path.fill()
.foregroundColor(self.accentColor)
.overlay(path.stroke(self.backgroundColor, lineWidth: 2))
.scaleEffect(self.show ? 1 : 0)
.animation(Animation.spring().delay(Double(self.index) * 0.04))
.onAppear {
self.show = true
}
}
}

### 折线图

``````var stepWidth: CGFloat {
if data.points.count < 2 {
return 0
}
return frame.size.width / CGFloat(data.points.count - 1)
}

``````var stepHeight: CGFloat {
var min: Double?
var max: Double?
let points = self.data.onlyPoints()
if minDataValue != nil && maxDataValue != nil {
min = minDataValue
max = maxDataValue
}else if let minPoint = points.min(), let maxPoint = points.max(), minPoint != maxPoint {
min = minPoint
max = maxPoint
}else {
return 0
}

if let min = min, let max = max, min != max{
if min <= 0 {
return (frame.size.height - padding) / CGFloat(max - min)
}else {
return (frame.size.height - padding) / CGFloat(max - min)
}
}

return 0
}

``````var p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)

``````let p2 = CGPoint(x: step.x * CGFloat(pointIndex), y: step.y*CGFloat(points[pointIndex]-offset))

``````ublic mutating func addQuadCurve(to p: CGPoint, control cp: CGPoint)

``````static func quadCurvedPathWithPoints(points:[Double], step:CGPoint, globalOffset: Double? = nil) -> Path {
var path = Path()
if (points.count < 2){
return path
}

let offset = globalOffset ?? points.min()!
var p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
path.move(to: p1)
for pointIndex in 1..<points.count {
let p2 = CGPoint(x: step.x * CGFloat(pointIndex), y: step.y*CGFloat(points[pointIndex]-offset))
let midPoint = CGPoint.midPointForPoints(p1: p1, p2: p2)
p1 = p2
}
return path
}

``````self.closedPath
.rotationEffect(.degrees(90), anchor: .center)
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
.transition(.opacity)
.animation(.easeIn(duration: 1.6))

self.path
.trim(from: 0, to: self.showFull ? 1:0)
.rotationEffect(.degrees(180), anchor: .center)
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
.animation(.easeOut(duration: 1.2).delay(Double(self.index) * 0.4))
.onAppear {
self.showFull = true
}
.onDisappear {
self.showFull = false
}

## 最后

**往期文章：

1. 阅读完记得给我点个赞哦，有👍 有动力
2. 关注公众号--- HelloWorld杰少，第一时间推送新姿势

iOS