最近我和两个朋友参加了SwiftUI Jam。有一个周末,我们在 SwiftUI 中开发了一个小应用程序,这很有趣。
应用程序的仪表板显示多个统计信息,其中之一显示为饼图。
在下面,我想向您展示我们在 SwiftUI 中制作一个简单的可重用饼图的方法。
首先,我将向您展示如何制作具有固定值的饼图,以了解如何绘制切片。在文章的后面,我将向您展示我们使饼图更易于使用和响应其大小的方法。
画一个圆圈
在 SwiftUI 中绘制一个圆圈再简单不过了。
Circle()
就是这样,但这不会让我们走得太远,所以我们真正需要的是绘制一个我们想要的任何大小的馅饼。首先,让我们来看看如何绘制例如半圆。为此,我们将使用路径。
我不会向您展示路径通常是如何工作的,但是使用路径和弧线,您可以绘制比圆复杂得多的形式。
画一个半圆
下面的代码创建一个半径为 100 的半圆,从 0 度到 180 度。
.move将路径移动到正确的起点。.addArc在给定半径的两个角度之间围绕给定中心绘制一条弧线。
这就是绘制半圆所需的全部内容。
Path { path in
path.move(to: CGPoint(x: 200, y: 200))
path.addArc(center: CGPoint(x: 200, y: 200),
radius: 100,
startAngle: Angle(degrees: 0),
endAngle: Angle(degrees: 180),
clockwise: false)
}
.fill(Color.red)
有时可能有点棘手,至少对我来说是顺时针的含义。
在Apple官方文档中,您可以查看圆的图像,以了解圆的哪个位置处的度数。顺时针定义为’绘制弧线的方向’。因此,像我这样的人会想到一个我们都知道的时钟,并期望上面示例中的代码以顺时针为 false 绘制一个半圆,上半部分填充为红色,但情况完全相反,您将得到下半部分半圈。这是一个有点有点混乱,可能与方式与在此解释SwiftUI手柄坐标系发生冲突的答案被抢劫,mayoff。
对于以下内容,这将无关紧要,因为我们将只使用提供给我们的内容。我将选择顺时针为假,我确定这也可以用顺时针为真来完成,但您稍后必须调整计算。
绘制具有固定值的饼图
但在我们开始花哨的计算之前,让我们绘制我们的第一个饼图。
ZStack(alignment: .center) {
Path { path in
path.addArc(center: CGPoint(x: 200, y: 200),
radius: 100,
startAngle: Angle(degrees: 0),
endAngle: Angle(degrees: 180),
clockwise: false)
}
.fill(Color.red)
Path { path in
path.addArc(center: CGPoint(x: 200, y: 200),
radius: 100,
startAngle: Angle(degrees: 180),
endAngle: Angle(degrees: 360),
clockwise: false)
}
.fill(Color.blue)
}
我们将示例中的Path放在ZStack 中,将多个元素放在一起,并调整第二个Path的开始和结束角度,这会产生一个有两半的饼图。
对于固定值,这就是你所需要的,但在大多数情况下,显示的值不是固定的,所以让我们看看我们如何处理这个问题。
简化用户界面
我们的饼图将存在多个切片,我最终将它们称为PieSlice。在上面的示例中,每个Path都是一个PieSlice,因此我们将其包装在一个结构中:
private struct PieSlice : Hashable {
var start: Angle
var end: Angle
var color: Color
}
在视图的主体中,我们现在可以迭代(这就是为什么我们需要PieSlice来执行Hashable)饼切片,删除固定角度和颜色。
struct PieChart: View {
private var slices = [
PieSlice(start: Angle(degrees: 0),
end: Angle(degrees: 180),
color: .red),
PieSlice(start: Angle(degrees: 180),
end: Angle(degrees: 360),
color: .blue)
]
var body: some View {
ZStack(alignment: .center) {
ForEach(slices, id: \.self) { slice in
Path { path in
path.move(to: CGPoint(x: 200, y: 200))
path.addArc(center: CGPoint(x: 200, y: 200),
radius: 100,
startAngle: slice.start,
endAngle: slice.end,
clockwise: false
)
}
.fill(slice.color)
}
}
}
}
private struct PieSlice : Hashable {
var start: Angle
var end: Angle
var color: Color
}
现在可以使用更多切片轻松扩展饼图。半径和中心点的固定值稍后将被删除,我们现在将进行一些计算以摆脱固定角度。
计算
在创建某种可重复使用的组件时,“吃自己的狗粮”这句话总是出现在我的脑海中。所以我想让我的饼图尽可能容易使用。我们必须提供的最少信息是值和值应显示的颜色(如果颜色可以是随机的,您也可以想出一些预定义的颜色或 RandomColorGenerator)。
init(_ values: [(Color, Double)]) {
slices = calculateSlices(from: values)
}
// Usage for the example above
PieChart([
(.red, 50),
(.blue, 50)
])
这是我们必须进行计算的地方。我们有应该显示的给定值,我们需要将这些值转换为我们的切片。这将在calculateSlices函数中完成。
private func calculateSlices(from inputValues: [(color: Color, value: Double)]) -> [PieSlice] {
// 1
let sumOfAllValues = inputValues.reduce(0) { $0 + $1.value }
guard sumOfAllValues > 0 else {
return []
}
let degreeForOneValue = 360.0 / sumOfAllValues
// 2
var slices = [PieSlice]()
var currentStartAngle = 0.0
inputValues.forEach { inputValue in
let endAngle = degreeForOneValue * inputValue.value +
currentStartAngle
slices.append(
PieSlice(
start: Angle(degrees: currentStartAngle),
end: Angle(degrees: endAngle),
color: inputValue.color
)
)
currentStartAngle = endAngle
}
return slices
}
乍一看这似乎有点复杂,但让我们分解一下。
在第一部分中进行了两个计算。
首先我们得到所有值的总和。然后我们将除以这个总和 360.0 度。
这将为我们提供显示单个值所需的度数。
例如,输入值为 20、30 和 50。这些值的总和为 100。
let degreeForOneValue = 360.0 / 100
// Will be 3.6
在本例中,我们需要 3.6 度来显示值 1。对于 50,我们需要3,6 * 50,即180,这将占据圆的一半。
在计算的第二部分中,我们迭代输入值并将它们映射到饼图切片。除了将值转换为度数之外,我们还必须处理每个角度的起点。每个PieSlice应该从前一个PieSlice的结束角度开始,这是用currentStartAngle处理的,它会被添加到每个计算出的结束角度。在每次迭代结束时currentStartAngle将被设置到endAngle所计算的PieSlice。
这就是我们创建一个非常易于使用的饼图所需要做的一切。上面有两个半圆的例子现在可以这样实现:
PieChart([
(.red, 50),
(.blue, 50),
])
// or this
PieChart([
(.red, 1),
(.blue, 1),
])
我们现在可以创建任何饼图。但是我们还没有完成,我们的饼图在值上是动态的,但它的大小非常固定,具有硬编码的中心点和固定为 100 的半径。
饼图的响应大小
我们可以通过将ZStack包装到GeometryReader 中来获取父视图的大小。我们的饼图将被放置在某个视图中,因此它将是某种矩形。这个矩形可以有不同的高度和宽度,我们希望在其中使用尽可能多的空间而不重叠。
那么我们需要什么?首先是矩形的中心。其次是饼图必须适合矩形的半径。
中心是 x 宽度的一半和 y 高度的一半。这些值可以在GeometryReader上访问。
矩形的高度和宽度很可能不是相同的值,除了正方形,我们必须使用两个值中较小的值作为半径,然后饼图具有尽可能大的尺寸而不会重叠。
GeometryReader { reader in
let halfWidth = (reader.size.width / 2)
let halfHeight = (reader.size.height / 2)
let radius = min(halfWidth, halfHeight)
let center = CGPoint(x: halfWidth, y: halfHeight)
ZStack(alignment: .center) {
ForEach(slices, id: \.self) { slice in
Path { path in
path.move(to: center)
path.addArc(center: center,
radius: radius,
startAngle: slice.start ,
endAngle: slice.end,
顺时针: false
)
}
.fill(slice.color)
}
}
}
精品教程推荐
加入我们一起学习SwiftUI
QQ:3365059189
SwiftUI技术交流QQ群:518696470
教程网站:www.openswiftui.com