WWDC22- Swift Charts初探

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开发需求的小伙伴,建议持续保持关注。

参考文献

猜你喜欢

转载自juejin.im/post/7130969087621988389