Olaf⛄️ 2022-8-12终稿
前言
作为一个iOS开发者,我们在日常开发中,多多少少会遇到图标展示类的需求,相信大家解决图标需求时,都会直接使用或者参考github开源的图表绘制框架:Charts,这个基于CoreGraphics绘制图表的框架,支撑了相当一部分的图表开发需求。
2022,Apple自己的图表框架来了!
全新的 Swift Charts 框架在WWDC22中跟开发者见面了。这是一个强大而简洁的SwiftUI框架,能将数据转换为信息丰富的可自定义可视化图表。下面是相关的4个Session:
强力建议观看Session!!!
框架特性
上图是Seesion中的效果呈现、展示了Swift Charts强大的绘制能力,从简单的柱状图到复杂的向量图,热力图,Swift Charts都可以用简洁的代码实现。Swift Charts 支持 localization 本地化 和 accessibility 辅助功能。还可以使用图表修饰符覆盖默认行为以自定义图表。 例如,可以通过向图表添加动画来创建动态体验。
兼容性
iOS 16+、iPadOS 16.0+、macOS 13.0+、watchOS 9.0+
Marks
-
BarMark
可以使用BarMark创建不同类型的条形图,如柱状图,进度条图等。
-
LineMark
可以通过绘制类别或日期属性(通常使用 x 位置)和绘制数字类别(通常使用 y 位置)来创建折线图。
-
PointMark
可以使用 PointMark 图表内容创建不同类型的点图表。可以使用点标记构建的一个常见图表是散点图,它显示两个数值数据属性之间的关系。
-
AreaMark
可以使用 AreaMark 将数据可视化为单个区域形状。 可以使用 AreaMark 图表内容创建不同类型的面积图。 要创建简单的区域标记图表,我们通常会将日期或有序字符串属性绘制到 x 位置,将数字绘制到 y 位置。
-
RuleMark
可以使用 RuleMark 在图表中绘制水平或垂直规则,如平均线,阈值线等。
-
RectangleMark
可以使用RectangleMark将数据字段映射成矩形图。 可以使用RectangleMark创建热图图表或注释图表中的矩形区域。
以上就是Swift Charts所有的Mark,它们可以单独使用,也可以组合使用,从而支撑开发者按需求开发出所需要的图表。用Session中的图来总结这些Mark吧
框架实践
实现Swift Charts框架,首先需要对SwiftUI的语法有一定的了解,下面将挑选典型图表进行coding实践。
BarkMark
如果我们需要用一张图表来直观的呈现多个国家的某一项开支费用,那我们就可以直接用BarMark来构建一个简单的柱状图来实现。
struct BarMarkData: Identifiable {
let name: String
let count: Double
var id: String{name}
}
let defData: [BarMarkData] = [
BarMarkData(name: "China", count: 15000),
BarMarkData(name: "US", count: 30000),
BarMarkData(name: "UK", count: 2000),
BarMarkData(name: "Japan", count: 800),
BarMarkData(name: "France", count: 3000)
]
struct BarMarkChartView: View {
var body: some View {
Chart(data) {
BarMark(
x: .value("name", $0.name),
y: .value("count", $0.count)
)
}.frame(width: 360, height: 300)
}
}
struct BarMarkChartView_Previews: PreviewProvider {
static var previews: some View {
AreaMarkChartView()
}
}
复制代码
我们看呈现效果:
如果这项开支还需要跟另一项开支进行对比呈现,这该怎么实现呢?我们只需要将上面的代码略微改动,即可实现,一下只粘贴新增部分的代码:
let eduData: [BarMarkData] = [
BarMarkData(name: "China", count: 28000),
BarMarkData(name: "US", count: 35000),
BarMarkData(name: "UK", count: 6000),
BarMarkData(name: "Japan", count: 2000),
BarMarkData(name: "France", count: 5000)
]
let mergeData = [
(outlay: "def", data: defData),
(outlay: "edu", data: eduData)
]
struct AreaMarkChartView: View {
var body: some View {
Chart(mergeData, id: .outlay) { mergeData in
ForEach(defData) { datum in
ForEach(mergeData.data, id: .id) { element in
BarMark(
x: .value("name", element.name),
y: .value("count", element.count)
).position(by: .value("outlay", mergeData.outlay))
.foregroundStyle(by: .value("outlay", mergeData.outlay))
}
}
}.frame(width: 360, height: 300)
}
}
复制代码
我们再来看看呈现效果:
现在已经实现了几个国家两个费用支出的对比图。对比上面,在代码上除了新增数据源,在Charts实现环节,也新增了position和foregroundStyle两个扩展方法
-
.position
position可以创建分组,让图表沿水平轴按其“类型”标记具有相同“产品”的mark。
-
.foregroundStyle
foregroundStyle按照传参不同,可进行不同类型的展示设置,如下:
extension ChartContent {
/// Sets the foreground style for marks in this chart content.
///
/// - Parameter color: The color.
public func foregroundStyle<S>( _ style: S) -> some ChartContent where S : ShapeStyle
/// Encodes data as the foreground style for marks in this chart content.
///
/// - Parameter data: The data property or value.
public func foregroundStyle<D>(by value: PlottableValue<D>) -> some ChartContent where D : Plottable
}
复制代码
LineMark
如果我们需要展示公司统计的一年内营收数据的变化,并且对比往年的数据,那此时我们可以用到的最简单的图就是线性图了,当然我们也可以使用其他的类型的图表来表示。下面我们先用LineMark来进行实践
struct SalesSummary: Identifiable {
let month: String
let total: Int
var id: String {month}
}
let pastData: [SalesSummary] = [
.init(month: "Jan", total: 3388),
.init(month: "Feb", total: 4420),
.init(month: "Mar", total: 6120),
...
.init(month: "Dec", total: 11076)
]
let lineData: [SalesSummary] = [
.init(month: "Jan", total: 5566),
.init(month: "Feb", total: 4610),
.init(month: "Mar", total: 5533),
...
.init(month: "Dec", total: 12998)
]
let salesData = [
(quarter: "2022", data: currentData),
(quarter: "2021", data: pastData)
]
struct LineMarkChartView: View {
var body: some View {
Chart{
ForEach(salesData, id: .years) { salesData in
ForEach(salesData.data, id: .id) { element in
LineMark(x: .value("month", element.month),
y: .value("total", element.total))
}.foregroundStyle(by: .value("sales", salesData.years))
}
}.frame(width: 360, height: 300)
}
}
struct LineMarkChartView_Previews: PreviewProvider {
static var previews: some View {
LineMarkChartView()
}
}
复制代码
我们来看效果:
如果想给折线图增加一个数据点的标记符号,同时让折线变的圆润,那我们就需要实现以下两个属性
struct LineMarkChartView: View {
var body: some View {
Chart{
ForEach(salesData, id: .years) { salesData in
ForEach(salesData.data, id: .id) { element in
LineMark(x: .value("month", element.month),
y: .value("total", element.total))
.interpolationMethod(.catmullRom)
.symbol(by: .value("sales", salesData.years))
}.foregroundStyle(by: .value("sales", salesData.years))
}
}.frame(width: 360, height: 300)
}
}
复制代码
再看下效果:
此时我们可以看到数据节点都用原点标记出来,并且折线也变成了更加柔和的曲线,这就是 interpolationMethod 和 symbol 这两个属性发挥的作用。
-
interpolationMethod
返回使用给定插值绘制数据的图表内容,包含多个,这里不一一列举了,可以查看api,这个方法仅提供给Line 和 Area 两种Mark
-
symbol
symbol根据传参不同有三个不同的方法实现,来支持不同的特性功能
extension ChartContent {
/// 设置此图表内容中标记的绘图符号类型
/// - Parameter symbol: The symbol.
public func symbol<S>( _ symbol: S) -> some ChartContent where S : ChartSymbolShape
/// 将数据编码为此图表内容中标记的符号。如上面的例子
/// - Parameter data: The data property or value.
public func symbol<D>(by value: PlottableValue<D>) -> some ChartContent where D : Plottable
/// 返回以给定视图作为绘图符号的图表内容。
/// - Parameter symbol: The view to use as the plotting symbol.
public func symbol<V>(@ViewBuilder symbol: () -> V) -> some ChartContent where V : View
}
复制代码
AreaMark
上面的公司年度营收的折线图数据同样也可以用来实践AreaMark区域图,我们来看看具体的代码实现,会不会更复杂呢,还是同样简单
let areaData: [BarMarkData] = [
.init(month: "Jan", total: 5566),
.init(month: "Feb", total: 4610),
.init(month: "Mar", total: 5533),
...
.init(month: "Dec", total: 12998)
]
struct AreaMarkChartView: View {
var body: some View {
Chart(areaData) { element in
AreaMark(
x: .value("month", element.month),
y: .value("total", element.total)
).interpolationMethod(.catmullRom)
.foregroundStyle(.pink)
}.frame(width: 360, height: 300)
}
}
复制代码
同样我们先来看看效果:
同样,AreaMark也使用了interpolationMethod来增加了边缘的曲线,同时还使用foregroundStyle来设置区域的颜色。
更多支持
Swift Charts可以三种数据类型作为他的数据值
-
Quantitative(数据型,如果Int、Double等)
-
Nominal(名义型,如各类名称标签)
-
Temporal(时间型,如年月周日时分等)
所以,6种Mark实际只有三种数据类别,每种Mark通常使用的属性通产个包括以下这些,这些属性,我们再上面的实践Demo中也都有使用到:
那么这些属性是不是足够了呢?
当然,是不够的。我们在实际开发中呈现的UI需求往往是定制化元素比较多的,现在我们来看看Swift Charts 框架都能够支持哪些内容的自定义,Seesion中给我们罗列了Chart的自定义内容维度:
接下来,我从途中所列的自定义属性入手,结合上面的的Demo例子,进一步实践Swift Charts的自定义能力。
增加Plot属性后,我们看看实际的效果
.chartPlotStyle { plot in
plot.background(.purple.opacity(0.8))
.border(.purple, width: 2)
.frame(width: 150, height: 100)
}
// 此时的效果,下图一。
// 如果将这个图标展示在Apple Watch,或者Widget上,
// 坐标线的效果此时会成为一种干扰,当然我们可以隐藏坐标线
.chartXAxis(.hidden)
.chartYAxis(.hidden)
// 此时的效果,下图二
复制代码
当然,关于这三个属性的应用,不止于此。它们都有很强大的自定义支撑能力来应对图标的展示需求,再结合Descriptions、Interaction、Color来实现更加酷炫的图表呈现。
关于使用
Charts的图表呈现能力虽然强大,但是我们都知道图表有它特殊的呈现场景,那么何时应该较交互呈现为图表,苹果的Session中已经给我们进行的设计指引。
当我们需要将数据进行变化、对比、进度(比例)呈现时,我们就可以使用Chart来帮我们进行可视化的实现 了。
如上三张图所示:
-
当我们需要可视化某项持续性数据变化趋势时,我们就可以使用Change部分的可视化方式来呈现。例如:用户单日步数、用户消费、浏览时长等数据变化趋势。
-
当我们需要可视化某项任务的进度,完成度、参与度、占比等等数据时,我们就可以使用Proportion部分的可视化形式来呈现。例如:用户年度App各类业务消费占比。
-
当我们需要可视化更全面数据的对比及跟维度的数据时,我们可以使用Comparison部分的可视化形式来呈现。如各公司的数据平台等系统的数据。
综上,Charts的使用场景,可能依然是众多App中的小部分,但是在特定行业App的业务中则是核心能力的展示,如你需要做一个股票或者财经类的App,又或者关于健康数据的App。Apple Watch的应用中则对Swift Charts有更多的需求。下面我们可以看看Session中的一些案例。
写在最后
Swift Charts是在WWDC22上和大家见面的,其强大的图表呈现能力,随着你对框架的挖掘和熟悉,会逐渐感知到。那么当你看了这篇文章后,你对Swift Charts有什么想法?
-
好家伙,兼容性最低iOS16,哪年那月才能用上...
-
基于SwiftUI,我还不如用danielgindi/Charts,毕竟SwiftUI也得最低iOS13的支持,等到那时候再看SwiftUI吧...
这些问题确确实实存在,而且当你熟悉Swift Charts后,也一定会发现有它所不能支撑的需求场景。Swift Charts在WWDC22和大家见面,我相信在WWDC23/24或者更远的将来,Swift Charts框架也会持续的更新和改进,有Widget Kit开发需求,或者Apple Watch开发需求的小伙伴,建议持续保持关注。