SwiftUI 中的饼图

最近我和两个朋友参加了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

发表回复