WWDC 2023 为我们带来了 iOS 17,也为我们带来了 SwiftUI 5.0。
在 SwiftUI 新版中,Apple 增加了很多重磅功能,也对原有功能做了大幅度升级。
对于 Charts 框架, 新增了饼图(Pie)类型并且加入了图表元素的原生选择功能。
在本篇博文中,就让我们一起来看看 SwiftUI 5.0 中这些激动人心的新功能吧!
Let’s go!!!
SwiftUI 5.0 在 4.0 众多图表类型基础之上,增加了全新的 饼图(Pie) 类型,我们可以通过它来更形象的展示图表数据。
注意:本文中的代码需要 Xcode 15 beta 版才能编译和运行。
下面是 SwiftUI 4.0 Charts 条状图的展示:
代码如下:
@Model
final class Item {
var name: String
var power: Int
var timestamp: Date
init(name: String, power: Int) {
self.name = name
self.power = power
timestamp = Date.now
}
}
Chart(items) { item in
BarMark(x: .value("power", item.power), stacking: .normalized)
.foregroundStyle(by: .value("name", item.name))
}
.chartLegend(.hidden)
想改为使用新饼图类型非常简单,只需将上面的 BarMark 换为 SectorMark 即可:
SectorMark(angle: .value("power", item.power))
我们可以调整每块“大饼”的空隙大小(angularInset)和圆角的弧度(cornerRadius):
SectorMark(angle: .value("power", item.power),angularInset: 3.0)
.cornerRadius(10)
值得注意的是:Charts 中饼图数据改变的动画效果做的也非常生动,SwiftUI 会自动根据状态的变化来合成自然的动画,无需多写半行代码。
不过,“大饼”虽好,“甜甜圈”更佳!
小孩子才做选择,光有“大饼”怎么行,我们连“甜甜圈”也统统都要了!
实现“甜甜圈”(饼图空心)效果也很容易,我们只需调整 SectorMark 构造器中 innerRadius 属性的值即可:
SectorMark(angle: .value("power", item.power),
innerRadius: .ratio(innerRadius),
angularInset: 3.0
)
好诱人的“甜甜圈”哦,有没有想吃的欲望呢?
除了加入新图表类型以外,SwiftUI 5.0 中 Charts 终于可以支持原生选择啦!
现在,我们无需再手动计算是图表中哪个元素被选中了,一切回归简洁:
struct LocationDetailsChart: View {
@Binding var rawSelectedDate: Date?
var body: some View {
Chart {
ForEach(data) { series in
ForEach(series.sales, id: \.day) { element in
LineMark(
x: .value("Day", element.day, unit: .day),
y: .value("Sales", element.sales)
)
}
.foregroundStyle(by: .value("City", series.city))
.symbol(by: .value("City", series.city))
.interpolationMethod(.catmullRom)
}
}
.chartXSelection(value: $rawSelectedDate)
}
}
如上代码所示,我们使用 chartXSelection(value:) 修改器方法将当前选中的数据放入指定的绑定($rawSelectedDate)中。
除了选择单个图表元素,我们还可以选择一段范围内的元素集合:
Chart(data) { series in
ForEach(series.sales, id: \.day) { element in
LineMark(
x: .value("Day", element.day, unit: .day),
y: .value("Sales", element.sales)
)
}
...
}
.chartXSelection(value: $rawSelectedDate)
.chartXSelection(range: $rawSelectedRange)
那么问题来了,能不能选中 SwiftUI 5.0 图表新饼图类型的“大饼”元素呢?答案是肯定的!
下面是官方视频中对应的代码:
Chart(data, id: \.name) { element in
SectorMark(
angle: .value("Sales", element.sales),
innerRadius: .ratio(0.618),
angularInset: 1.5
)
.cornerRadius(5)
.foregroundStyle(by: .value("Name", element.name))
.opacity(element.name == selectedName ? 1.0 : 0.3)
}
.chartAngleSelection(value: $selectedAngle)
类似的, 通过 chartAngleSelection(value:) 修改器方法实现了饼图元素的选中:
不过,单从这段代码我们还是无法了解饼图元素选中的实现细节,比如:selectedAngle 是什么?它是如何转换成 selectedName 的呢?
为什么 在此要“犹抱琵琶半遮面”隐藏相关的细节呢?这不禁让我预感到它会是一个“坑”!
“坑”中的实现很可能在 iOS 17 正式版中会有所不同,所以 才会这样“遮遮掩掩”。
想要了解更多相关的内容,请移步如下链接观赏:
WWDC 23 中对应内容的官方视频在下面,想要了解来龙去脉的小伙伴们可以“肆意”观赏:
尽管官方视频中的代码对如何完成饼图元素选中功能“闪烁其词”,但我们可以自己发挥“主观能动性”来大胆推测一下它的实现细节:即自己搞定“甜甜圈”的选中功能。
首先我们要搞清楚的是, chartAngleSelection 方法参数中的绑定值到底是个啥:
public func chartAngleSelection<P>(_ binding: Binding<P?>) -> some View where P : Plottable
我们可以通过监视 angleValue 的值,来看看它是如何跟随我们点击而变化的:
struct ContentView: View {
// 省略其它状态定义...
@Query private var items: [Item]
@State private var angleValue: Int?
var body: some View {
NavigationView {
List {
Chart(items) { item in
SectorMark(angle: .value("power", item.power),
innerRadius: .ratio(innerRadius),
angularInset: 3.0
)
.cornerRadius(10)
.foregroundStyle(by: .value("name", item.name))
}
.chartLegend(.hidden)
.chartAngleSelection($angleValue)
.onChange(of: angleValue){ old,new in
// 探查 angleValue 的真正面目...
print("new angle value: \(new)")
}.padding(.vertical, 50)
ForEach(items) { ... }
}
.navigationTitle("饼图演示")
}
}
}
如上图所示:chartAngleSelection($angleValue) 方法中的绑定是一个数量值(定义成浮点数类型也可以),我们还发现 angleValue 在 0° 位置附近点击时值越小,而在 360° 位置点击时值越大。
经过验证可得:angleValue 最大值就是 items 中所有元素 power 值的和!据此,我们可以轻松写一个从 angleValue 值找到对应选中 item 的方法:
private func findSltItem() -> Item? {
guard let slt = angleValue else { return nil }
var sum = 0
// 若 angleValue 小于第一个 item.power ,则表示选择的是图表中首张“大饼”!
var sltItem = items.first
for item in items {
sum += item.power
// 试探正确选中的饼图元素
if sum >= slt {
sltItem = item
break
}
}
return sltItem
}
我们现在可以根据饼图中当前选中的 angleValue 值,轻松找到对应的 Item 了:
struct ContentView: View {
// 省略其它状态定义...
@Query private var items: [Item]
@State private var angleValue: Int?
@State private var sltItem: Item?
var body: some View {
NavigationView {
List {
Chart(items) { item in
SectorMark(angle: .value("power", item.power),
innerRadius: .ratio(innerRadius),
angularInset: 3.0
)
.cornerRadius(10)
.foregroundStyle(by: .value("name", item.name))
.opacity(sltItem?.id == item.id ? 1.0 : 0.3)
}
.onChange(of: angleValue){ old,new in
withAnimation {
if let item = findSltItem() {
if item == sltItem {
// 点击已被选中的元素时取消选择
sltItem = nil
}else{
sltItem = item
}
}
}
}.padding(.vertical, 50)
ForEach(items) {...}
}
.navigationTitle("饼图演示")
}
}
}
效果如下:
看来为 WWDC 官方代码填坑的感觉也很不错哦
在本篇博文中,我们介绍了 WWDC 23 最新 SwiftUI 5.0(iOS 17)中关于图表的新体验,学习了如何创建饼图(Pie)和实现 Charts 元素的选中功能,小伙伴们还不赶快操练起来!
感谢观赏,再会!