在 iOS16 中用 SwiftUI 图表定制一个线图

4,217 阅读6分钟

在 iOS 16 中引入的 SwiftUI 图表,可以以直观的视觉格式呈现数据,并且可以使用 SwiftUI 图表快速创建。本文演示了几种定制折线图并与区域图结合来展示数据的方法。

系列文章

  1. 如何在 SwiftUI 中创建条形图
  2. SwiftUI 中的水平条形图
  3. 在 iOS 16 中用 SwiftUI Charts 创建一个折线图
  4. 在 iOS16 中用 SwiftUI 图表定制一个线图
  5. 在 Swift 图表中使用 Foudation 库中的测量类型

默认折线图

上周发布的 在 iOS 16 中用 SwiftUI Charts 创建一个折线图中使用 SwiftUI Charts 创建默认折线图开始。这显示了两个不同星期的步数数据,比较了每个工作日的步数。

struct ChartView1: View {
    var body: some View {
        VStack {
            GroupBox ( "Line Chart - Daily Step Count") {
                Chart {
                    ForEach(stepData, id: \.period) { steps in
                        ForEach(steps.data) {
                            LineMark(
                                x: .value("Week Day", $0.shortDay),
                                y: .value("Step Count", $0.steps)
                            )
                            .foregroundStyle(by: .value("Week", steps.period))
                            .accessibilityLabel("\($0.weekdayString)")
                            .accessibilityValue("\($0.steps) Steps")
                        }
                    }
                }
                .frame(height:400)
            }
            .padding()
            
            Spacer()
        }
    }
}

使用 SwiftUI 图表创建的默认折线图

改变图表背后的背景

技术上讲,这与图表无关,但 GroupBox 的背景可以用颜色或 GroupBoxStyle 来设置。

struct YellowGroupBoxStyle: GroupBoxStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.content
            .padding(.top, 30)
            .padding(20)
            .background(Color(hue: 0.10, saturation: 0.10, brightness: 0.98))
            .cornerRadius(20)
            .overlay(
                configuration.label.padding(10),
                alignment: .topLeading
            )
    }
}
struct ChartView2: View {
    var body: some View {
        VStack {
            GroupBox ( "Line Chart - Daily Step Count") {
                Chart {
                    ForEach(stepData, id: \.period) { steps in
                        ForEach(steps.data) {
                            LineMark(
                                x: .value("Week Day", $0.shortDay),
                                y: .value("Step Count", $0.steps)
                            )
                            .foregroundStyle(by: .value("Week", steps.period))
                            .accessibilityLabel("\($0.weekdayString)")
                            .accessibilityValue("\($0.steps) Steps")
                        }
                    }
                }
                .frame(height:400)
            }
            // Add a style to the GroupBox
            .groupBoxStyle(YellowGroupBoxStyle())
            .padding()
            
            Spacer()
        }
    }
}

为 GroupBox 背景设置样式

设置绘图或图表的背景

可以使用 chartPlotStyle 为图表绘图区域设置背景,或者使用 chartBackground 为整个图表设置一个背景。

设置绘图区域背景

GroupBox ( "Line Chart - Plot Background") {
    Chart {
        ForEach(stepData, id: \.period) { steps in
            ForEach(steps.data) {
                LineMark(
                    x: .value("Week Day", $0.shortDay),
                    y: .value("Step Count", $0.steps)
                )
                .foregroundStyle(by: .value("Week", steps.period))
                .accessibilityLabel("\($0.weekdayString)")
                .accessibilityValue("\($0.steps) Steps")
            }
        }
    }
    .chartPlotStyle { plotArea in
        plotArea
            .background(.orange.opacity(0.1))
            .border(.orange, width: 2)
    }
    .frame(height:200)
}
.groupBoxStyle(YellowGroupBoxStyle())

设置图表背景

GroupBox ( "Line Chart - Chart Background") {
    Chart {
        ForEach(stepData, id: \.period) { steps in
            ForEach(steps.data) {
                LineMark(
                    x: .value("Week Day", $0.shortDay),
                    y: .value("Step Count", $0.steps)
                )
                .foregroundStyle(by: .value("Week", steps.period))
                .accessibilityLabel("\($0.weekdayString)")
                .accessibilityValue("\($0.steps) Steps")
            }
        }
    }
    .chartBackground { chartProxy in
        Color.red.opacity(0.1)
    }
    .frame(height:200)
}
.groupBoxStyle(YellowGroupBoxStyle())            

设置绘图区域和图表的背景

GroupBox ( "Line Chart - Plot & Chart Backgroundt") {
    Chart {
        ForEach(stepData, id: \.period) { steps in
            ForEach(steps.data) {
                LineMark(
                    x: .value("Week Day", $0.shortDay),
                    y: .value("Step Count", $0.steps)
                )
                .foregroundStyle(by: .value("Week", steps.period))
                .accessibilityLabel("\($0.weekdayString)")
                .accessibilityValue("\($0.steps) Steps")
            }
        }
    }
    .chartBackground { chartProxy in
        Color.red.opacity(0.1)
    }
    .chartPlotStyle { plotArea in
        plotArea
            .background(.orange.opacity(0.1))
            .border(.orange, width: 2)
    }
    
    .frame(height:200)
}
.groupBoxStyle(YellowGroupBoxStyle())

使用 SwiftUI Charts 在绘图区域和全图表上设置背景

将 Y 轴移至左侧

将 Y 轴移至左侧边缘(leading)。

可以隐藏坐标轴或调整坐标轴的位置,比如将 Y 轴放在图表的左侧(leading)。y 轴默认显示在图表的右方(trailing)。可以使用 chartYAxisAxisMarks 将其放置在左侧。也可以通过设置可见性属性为隐藏来完全隐藏轴。

struct ChartView4: View {
    var body: some View {
        VStack {
            GroupBox ( "Line Chart - Y-axis on leading edge") {
                Chart {
                    ForEach(stepData, id: \.period) { steps in
                        ForEach(steps.data) {
                            LineMark(
                                x: .value("Week Day", $0.shortDay),
                                y: .value("Step Count", $0.steps)
                            )
                            .foregroundStyle(by: .value("Week", steps.period))
                            .accessibilityLabel("\($0.weekdayString)")
                            .accessibilityValue("\($0.steps) Steps")
                        }
                    }
                }
                .chartPlotStyle { plotArea in
                    plotArea
                        .background(Color(hue: 0.12, saturation: 0.10, brightness: 0.92))
                }
                // Place the y-axis on the leading side of the chart
                .chartYAxis {
                   AxisMarks(position: .leading)
                }
                .frame(height:400)
            }
            .groupBoxStyle(YellowGroupBoxStyle())
            .padding()
            
            Spacer()
        }
    }
}

使用 SwiftUI 图表将 Y 轴置于图表的左侧

移动图表的图例

图表图例默认显示在图表的底部。图例可以放在图表的任何一面,也可以放在图表的多个位置上。

GroupBox ( "Line Chart - legend overlay on top center") {
    Chart {
        ForEach(stepData, id: \.period) { steps in
            ForEach(steps.data) {
                LineMark(
                    x: .value("Week Day", $0.shortDay),
                    y: .value("Step Count", $0.steps)
                )
                .foregroundStyle(by: .value("Week", steps.period))
                .accessibilityLabel("\($0.weekdayString)")
                .accessibilityValue("\($0.steps) Steps")
            }
        }
    }
    // Position the Legend
    .chartLegend(position: .overlay, alignment: .top)
    .chartPlotStyle { plotArea in
        plotArea
            .background(Color(hue: 0.12, saturation: 0.10, brightness: 0.92))
    }
    .chartYAxis() {
        AxisMarks(position: .leading)
    }
    .frame(height:200)
}
.groupBoxStyle(YellowGroupBoxStyle())
GroupBox ( "Line Chart - legend trailing center") {
    Chart {
        ForEach(stepData, id: \.period) { steps in
            ForEach(steps.data) {
                LineMark(
                    x: .value("Week Day", $0.shortDay),
                    y: .value("Step Count", $0.steps)
                )
                .foregroundStyle(by: .value("Week", steps.period))
                .accessibilityLabel("\($0.weekdayString)")
                .accessibilityValue("\($0.steps) Steps")
            }
        }
    }
    // Position the Legend
    .chartLegend(position: .trailing, alignment: .center, spacing: 10)
    .chartPlotStyle { plotArea in
        plotArea
            .background(Color(hue: 0.12, saturation: 0.10, brightness: 0.92))
    }
    .chartYAxis() {
        AxisMarks(position: .leading)
    }
    .frame(height:200)
}
.groupBoxStyle(YellowGroupBoxStyle())

改变折线线型

折线图用一条直线将图表上的数据点连接起来。插值方法(interpolationMethod)函数可以用各种方式将数据点通过曲线连接。

struct ChartView6: View {
    var body: some View {
        VStack(spacing:30) {
            GroupBox ( "Line Chart - Curved line connector") {
                Chart {
                    ForEach(stepData, id: \.period) { steps in
                        ForEach(steps.data) {
                            LineMark(
                                x: .value("Week Day", $0.shortDay),
                                y: .value("Step Count", $0.steps)
                            )
                            .foregroundStyle(by: .value("Week", steps.period))
                            // Use curved line to join points
                            .interpolationMethod(.catmullRom)
                            .accessibilityLabel("\($0.weekdayString)")
                            .accessibilityValue("\($0.steps) Steps")
                        }
                    }
                }
                .chartLegend(position: .overlay, alignment: .top)
                .chartPlotStyle { plotArea in
                    plotArea
                        .background(Color(hue: 0.12, saturation: 0.10, brightness: 0.92))
                }
                .chartYAxis() {
                    AxisMarks(position: .leading)
                }
                .frame(height:300)
            }
            .groupBoxStyle(YellowGroupBoxStyle())
            
            GroupBox ( "Line Chart - Step line connector") {
                Chart {
                    ForEach(stepData, id: \.period) { steps in
                        ForEach(steps.data) {
                            LineMark(
                                x: .value("Week Day", $0.shortDay),
                                y: .value("Step Count", $0.steps)
                            )
                            .foregroundStyle(by: .value("Week", steps.period))
                            // Use step line to join points
                            .interpolationMethod(.stepCenter)
                            .accessibilityLabel("\($0.weekdayString)")
                            .accessibilityValue("\($0.steps) Steps")
                        }
                    }
                }
                .chartLegend(position: .overlay, alignment: .top)
                .chartPlotStyle { plotArea in
                    plotArea
                        .background(Color(hue: 0.12, saturation: 0.10, brightness: 0.92))
                }
                .chartYAxis() {
                    AxisMarks(position: .leading)
                }
                .frame(height:300)
            }
            .groupBoxStyle(YellowGroupBoxStyle())
            

            Spacer()
        }
        .padding()
    }
}

在 SwiftUI 图表中更改将数据点连接线型

改变折线的颜色

可以使用chartForegroundStyleScale来设置线形图中线条的默认颜色。

struct ChartView7: View {
    var body: some View {
        VStack() {
            GroupBox ( "Line Chart - Custom line colors") {
                Chart {
                    ForEach(stepData, id: \.period) { steps in
                        ForEach(steps.data) {
                            LineMark(
                                x: .value("Week Day", $0.shortDay),
                                y: .value("Step Count", $0.steps)
                            )
                            .foregroundStyle(by: .value("Week", steps.period))
                            .interpolationMethod(.catmullRom)
                            .symbol(by: .value("Week", steps.period))
                            .symbolSize(30)
                            .accessibilityLabel("\($0.weekdayString)")
                            .accessibilityValue("\($0.steps) Steps")
                        }
                    }
                }
                // Set color for each data in the chart
                .chartForegroundStyleScale([
                    "Current Week" : Color(hue: 0.33, saturation: 0.81, brightness: 0.76),
                    "Previous Week": Color(hue: 0.69, saturation: 0.19, brightness: 0.79)
                ])
                .chartLegend(position: .overlay, alignment: .top)
                .chartPlotStyle { plotArea in
                    plotArea
                        .background(Color(hue: 0.12, saturation: 0.10, brightness: 0.92))
                }
                .chartYAxis() {
                    AxisMarks(position: .leading)
                }
                .frame(height:400)
            }
            .groupBoxStyle(YellowGroupBoxStyle())
           
            Spacer()
        }
        .padding()
    }
}

为 SwiftUI 图表中的线条设置自定义颜色

改变折线风格

线形图上的线条可以通过使用StrokeStyle设置 lineStyle 来修改。在步骤数据中使用了两种不同的风格,以区分前一周的数据和当前的数据。此外,还为图表上的数据点设置了一个自定义符号。

struct ChartView8: View {
    
    let prevColor = Color(hue: 0.69, saturation: 0.19, brightness: 0.79)
    let curColor = Color(hue: 0.33, saturation: 0.81, brightness: 0.76)
    
    var body: some View {
        VStack() {
            GroupBox ( "Line Chart - Line color and format") {
                Chart {
                    ForEach(previousWeek) {
                        LineMark(
                            x: .value("Week Day", $0.shortDay),
                            y: .value("Step Count", $0.steps)
                        )
                        .interpolationMethod(.catmullRom)
                        .foregroundStyle(prevColor)
                        .foregroundStyle(by: .value("Week", "Previous Week"))
                        .lineStyle(StrokeStyle(lineWidth: 3, dash: [5, 10]))
                        .symbol() {
                            Rectangle()
                                .fill(prevColor)
                                .frame(width: 8, height: 8)
                        }
                        .symbolSize(30)
                        .accessibilityLabel("\($0.weekdayString)")
                        .accessibilityValue("\($0.steps) Steps")
                    }
                    
                    ForEach(currentWeek) {
                        LineMark(
                            x: .value("Week Day", $0.shortDay),
                            y: .value("Step Count", $0.steps)
                        )
                        .interpolationMethod(.catmullRom)
                        .foregroundStyle(curColor)
                        .foregroundStyle(by: .value("Week", "Current Week"))
                        .lineStyle(StrokeStyle(lineWidth: 3))
                        .symbol() {
                            Circle()
                                .fill(curColor)
                                .frame(width: 10)
                        }
                        .symbolSize(30)
                        .accessibilityLabel("\($0.weekdayString)")
                        .accessibilityValue("\($0.steps) Steps")
                    }
                }
                // Set the Y axis scale
                .chartYScale(domain: 0...30000)
                .chartForegroundStyleScale([
                    "Current Week" : curColor,
                    "Previous Week": prevColor
                ])
                .chartLegend(position: .overlay, alignment: .top)
                .chartPlotStyle { plotArea in
                    plotArea
                        .background(Color(hue: 0.12, saturation: 0.10, brightness: 0.92))
                }
                .chartYAxis() {
                    AxisMarks(position: .leading)
                }
                .frame(height:400)
            }
            .groupBoxStyle(YellowGroupBoxStyle())
            Spacer()
        }
        .padding()
    }
}

为 SwiftUI 图表中的一个数据集设置自定义线型

结合面积图和折线图

最后,将折线图与面积图结合起来,帮助区分一个数据集与另一个数据集。区域图只为当前一周的数据添加,并且区域的颜色被设置为渐变的线下。

struct ChartView9: View {
        
    var body: some View {
    
        let prevColor = Color(hue: 0.69, saturation: 0.19, brightness: 0.79)
        let curColor = Color(hue: 0.33, saturation: 0.81, brightness: 0.76)
        let curGradient = LinearGradient(
            gradient: Gradient (
                colors: [
                    curColor.opacity(0.5),
                    curColor.opacity(0.2),
                    curColor.opacity(0.05),
                ]
            ),
            startPoint: .top,
            endPoint: .bottom
        )

        VStack() {
            GroupBox ( "Line Chart - Combine LIne and Area chart") {
                Chart {
                    ForEach(previousWeek) {
                        LineMark(
                            x: .value("Week Day", $0.shortDay),
                            y: .value("Step Count", $0.steps)
                        )
                        .interpolationMethod(.catmullRom)
                        .foregroundStyle(prevColor)
                        .foregroundStyle(by: .value("Week", "Previous Week"))
                        .lineStyle(StrokeStyle(lineWidth: 3, dash: [5, 10]))
                        .symbol() {
                            Rectangle()
                                .fill(prevColor)
                                .frame(width: 8, height: 8)
                        }
                        .symbolSize(30)
                        .accessibilityLabel("\($0.weekdayString)")
                        .accessibilityValue("\($0.steps) Steps")
                    }
                    
                    ForEach(currentWeek) {
                        LineMark(
                            x: .value("Week Day", $0.shortDay),
                            y: .value("Step Count", $0.steps)
                        )
                        .interpolationMethod(.catmullRom)
                        .foregroundStyle(curColor)
                        .foregroundStyle(by: .value("Week", "Current Week"))
                        .lineStyle(StrokeStyle(lineWidth: 3))
                        .symbol() {
                            Circle()
                                .fill(curColor)
                                .frame(width: 10)
                        }
                        .symbolSize(30)
                        .accessibilityLabel("\($0.weekdayString)")
                        .accessibilityValue("\($0.steps) Steps")

                        AreaMark(
                            x: .value("Week Day", $0.shortDay),
                            y: .value("Step Count", $0.steps)
                        )
                        .interpolationMethod(.catmullRom)
                        .foregroundStyle(curGradient)
                        .foregroundStyle(by: .value("Week", "Current Week"))
                        .accessibilityLabel("\($0.weekdayString)")
                        .accessibilityValue("\($0.steps) Steps")
                        
                    }
                }
                // Set the Y axis scale
                .chartYScale(domain: 0...30000)
                
                .chartForegroundStyleScale([
                    "Current Week" : curColor,
                    "Previous Week": prevColor
                ])
                .chartLegend(position: .overlay, alignment: .top)
                .chartPlotStyle { plotArea in
                    plotArea
                        .background(Color(hue: 0.12, saturation: 0.10, brightness: 0.92))
                }
                .chartYAxis() {
                    AxisMarks(position: .leading)
                }
                .frame(height:400)
            }
            .groupBoxStyle(YellowGroupBoxStyle())
            Spacer()
        }
        .padding()
    }
}

在SwiftUI图表中使用自定义颜色将折线图与面积图结合起来

结论

SwiftUI Charts目前处于测试阶段,在Xcode性能和编译一些图表选项方面可能会有一些问题,但它很容易就能开始使用图表。帮助文档是可用的,而且很好,但我希望看到更多的代码示例。它是有很大的潜力来定制图表然后以直观的方式向应用程序的用户展示数据。

本文正在参加「金石计划 . 瓜分6万现金大奖」