WWDC22: Swift Charts 初探

发布
更新
字数 1418
阅读 8 分钟
阅读量 1346

Swift Charts 是由 Apple 提供的新框架,用来展示数据图表信息,基于 SwiftUI 实现。

Marks

Swift Charts 基本视觉元素被称之为 marks ,例如柱状图的柱等。

第一个例子

销售情况

import Charts
import SwiftUI

struct StlesDetailsChart: View {
    var body: some View {
        Chart {
            BarMark(
                // `.value` 工厂方法
                // 第一个参数是标签描述
                // 第二个则是标签值
                // 所以此处,x 轴按名称分组
                x: .value("Name", "Cachapa"),
                // y 值
                // 此处使用交易量
                // `.value` 方法会自动根据坐标系
                // 计算 bar 元素的高度,并加入等高线
                y: .value("Sales", 916)
            )
            // 第二个 bar
            // 多个 `marks` 会自动布局
            BarMark(
                x: .value("Name", "Injera"),
                y: .value("Sales", 850)
            )
        }
    }
}

注意 .value 方法的第一个参数,这里都是一致的,试一下把 x 值改成不一样的 label name 会有什么效果。

更多数据

通常我们的数据都是来自一个结合,如数组,所以我们需要更新一下我们的数据结构和显示图表的逻辑。接下来,我们会:

  1. 创建一个 Pancakges 结构体用来描述其销售情况
  2. 然后我们就可以得到一个销售列表:一个包含 Pancakges 的数组
  3. 最后我们会用 ForEach 循环创建柱状图来对比不同 pancake 的销售情况
import Charts
import SwiftUI

// 1. Pancakes 结构体
// 我们需要在 for 循环中使用,所以需要遵循 `Identifiable` 协议
struct Pancakges: Identifiable {
    // 这里的 name 和 sales 对应前面 `BarMark` 的 x, y label name
    let name: String
    let sales: Int
    
    // `Identifiable`
    // 直接使用 name 即可
    var id: String { name }
}

// 2. 销售列表
let sales: [Pancakes] = [
    .init(name: "Cachapa", salse: 916),
    .init(name: "Injera", salse: 850),
    .init(name: "Crêpe", salse: 802),
]

struct StlesDetailsChart: View {
    var body: some View {
        Chart {
            ForEach(sales) { element in
                BarMark(
                    x: .value("Name", element.name),
                    y: .value("Sales", element.sales)
                )
            }
        }
    }
}

之后我们可以添加更多的数据

let sales: [Pancakes] = [
    .init(name: "Cachapa", salse: 916),
    .init(name: "Injera", salse: 850),
    .init(name: "Crêpe", salse: 802),
    .init(name: "Jian Bing", salse: 753),
    .init(name: "Dosa", salse: 654),
    .init(name: "American", salse: 618),
]

然后交换一下 x y 的值

BarMark(
    x: .value("Sales", element.sales),
    y: .value("Name", element.name)
)

可以看到柱状图横过来了。为了达到最美观的效果,Swift Charts 会自动选择合适的坐标系风格。同时自动适配暗黑模式、系统字体大小、设备尺寸、方向以及可用性支持,如 VoiceOver 支持。

什么时候去哪里更好卖?

为了找出一周不同时间的最佳售卖地点,我们需要一个新的图表可视化的呈现出,不同城市在同一时间的销售对比情况,所以需要以下步骤:

  1. 定义一个销售摘要结构体,描述不同日期的销售情况
  2. 找出不同城市一周的销售数据
  3. 创建图表
import Charts
import SwiftUI

// 1. 销售摘要
struct SalesSummary: Identifiable {
    let weekday: Date
    let sales: Int
    
    var id: Date { weekday }
}

// 2. 数据集
let cupertinoData: [SalesSummary] = [
    /// Monday
    .init(weekday: date(2022, 5, 2), sales: 54),
    /// Tuesday
    .init(weekday: date(2022, 5, 3), sales: 42),
    /// Wednesday
    .init(weekday: date(2022, 5, 4), sales: 88),
    /// Thursday
    .init(weekday: date(2022, 5, 5), sales: 49),
    /// Friday
    .init(weekday: date(2022, 5, 6), sales: 42),
    /// Saturday
    .init(weekday: date(2022, 5, 7), sales: 125),
    /// Sunday
    .init(weekday: date(2022, 5, 8), sales: 67),
]

// 3. 可视化
struct CupertinoDetailsChart: View {
    var body: some View {
        // 把数据集作为参数传入,循环应用 element
        Chart(cupertinoData) { element in
            BarMark(
                // x 周显示日期,可以指定 `unit` 参数
                x: .value("Day", element.weekday, unit: .day),
                // 销售情况
                y: .value("Sales", element.sales)
            )
        }
    }
}

为了能在不同城市间切换查看销售数据,我们需要优化一下显示的逻辑,使得其可以根据数据集的变化而调整图表。所以这里会:

  1. 增加一个新的城市数据集作为示例
  2. 创建一个 State 变量,用来保存当前查看的城市
// 1. 增加数据集
let sfData: [SalesSummary] = [
    .init(weekday: date(2022, 5, 2), sales: 81),
    .init(weekday: date(2022, 5, 3), sales: 90),
    .init(weekday: date(2022, 5, 4), sales: 52),
    .init(weekday: date(2022, 5, 5), sales: 72),
    .init(weekday: date(2022, 5, 6), sales: 84),
    .init(weekday: date(2022, 5, 7), sales: 84),
    .init(weekday: date(2022, 5, 8), sales: 137),
]

struct LocationsToggleChart: View {
    // 2. 城市变量
    @State var city: City = .cupertino
    
    var data: [SalesSummary] {
        switch city {
        case .cupertino:
            return cupertinoData
        case .sanFrancisco:
            return sfData
        }
    }
    
    var body: some View {
        Vstack {
            // 选择器,切换城市,同时触发动画效果
            Picker("City", selection: $city.animation(.easeInOut)) {
                Text("Cupertino").tag(City.cupertino)
                Text("San Francisco").tag(City.sanFrancisco)
            }
            .pickerStyle(.segmented)
            
            // 图表
            Chart(data) {element in
                BarMark(
                    // x 周显示日期,可以指定 `unit` 参数
                    x: .value("Day", element.weekday, unit: .day),
                    // 销售情况
                    y: .value("Sales", element.sales)
                )
            }
        }
    }

注意这里通过 State 变量添加修饰符的方式来创建动画 .animation(.easeInOut)

再加一层

为了更直观的对比两个城市的销售情况,我们把两个城市的数据放在一起绘制,对比查看一周内的销售趋势。

  1. 定义一个 Series 结构体,区分不同城市的销售数据
  2. 定义一个 Series 数据集,用来绘制图表
struct Series: Identifiable {
    let city: String
    let sales: [SalesSummary]
    
    var id: String { city }
}

let seriesData: [Series] = [
    .init(city: "Cupertino", sales: cupertinoData),
    .init(city: "San Francisco", sales: sfData),
]

struct LocationsDetailsChart: View {
    var body: some View {
        Chart(seriesData) { series in
            ForEach(series.sales) { element in
                BarMark(
                    x: .value("Day", element.weekday, unit: .day),
                    y: .value("Sales", element.sales)
                )
                // 用颜色区分不同城市
                .foregroundStyle(by: .value("City", series.city)
            }
        }
    }
}
            

试着把 BarMark 换为 PointMarkLineChart 可以得到不同的效果

struct LocationsDetailsChart: View {
    var body: some View {
        Chart(seriesData) { series in
            ForEach(series.sales) { element in
                LineMark(
                    x: .value("Day", element.weekday, unit: .day),
                    y: .value("Sales", element.sales)
                )
                .foregroundStyle(by: .value("City", series.city)
                
                PointMark(
                    x: .value("Day", element.weekday, unit: .day),
                    y: .value("Sales", element.sales)
                )
                .foregroundStyle(by: .value("City", series.city)
                .symbol(by: .value("City", series.city)
            }
        }
    }
}

本文根据 WWDC22 Hello Swift Charts 编写:https://developer.apple.com/videos/play/wwdc2022/10136/