You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在SwiftUI Charts中实现X轴时间范围平滑切换与刻度适配?

SwiftUI Charts 时间范围平滑切换实现方案

核心需求梳理

  • 按时间粒度(周/月)聚合数据:周视图展示每周求和值,月视图展示每月求和值
  • 实现类似iOS健康App的X轴时间范围平滑缩放(3个月↔9个月视图切换)
  • 动态适配X轴刻度:3个月视图显示每周第一天,9个月视图仅显示月份名称

代码修改实现

1. 拆分状态与新增数据聚合逻辑

先拆分原有的type变量,分别管理数据类型和时间范围,同时实现按周/月聚合数据的方法:

import SwiftUI
import Charts

// 时间范围枚举
enum TimeRange: Int {
    case threeMonths = 0
    case nineMonths = 1
    
    var dayCount: Int {
        switch self {
        case .threeMonths: return 90
        case .nineMonths: return 270
        }
    }
}

// 数据类型枚举
enum DataType: Int {
    case expense = 0
    case fuel = 1
    case mileage = 2
    case carWash = 3
    
    var tintColor: Color {
        switch self {
        case .expense: return .orange
        case .fuel: return .green
        case .mileage: return .blue
        case .carWash: return .purple
        }
    }
    
    func getValue(from entry: Entry) -> Double {
        switch self {
        case .expense: return entry.value ?? 0
        case .fuel: return entry.quantity ?? 0
        case .mileage: return entry.trip ?? 0
        case .carWash: return entry.quantity ?? 0
        }
    }
}

struct ChartsV: View {
    @State private var isSheetPresented = false
    @State var contentHasScrolled = false
    @ObservedObject var dataStore: DataStoreVM
    
    @State private var selectedDataType: DataType = .expense
    @State private var selectedTimeRange: TimeRange = .threeMonths

    init(dataStore: DataStoreVM) {
        self.dataStore = dataStore
        let navBarAppearance = UINavigationBarAppearance()
        UINavigationBar.appearance().standardAppearance = navBarAppearance
    }

    // 按时间粒度聚合数据
    private func aggregatedData() -> [(date: Date, total: Double)] {
        let calendar = Calendar.current
        // 过滤时间范围内的数据
        let cutoffDate = calendar.date(byAdding: .day, value: -selectedTimeRange.dayCount, to: Date())!
        let validEntries = dataStore.entries.filter { $0.date >= cutoffDate }
        
        // 按周/月分组
        let groupingUnit: Calendar.Component = selectedTimeRange == .threeMonths ? .weekOfYear : .month
        let groupedEntries = Dictionary(grouping: validEntries) { entry in
            calendar.startOfDay(for: calendar.date(from: calendar.dateComponents([groupingUnit, .year], from: entry.date))!)
        }
        
        // 分组求和
        return groupedEntries.map { (date, entries) in
            let sum = entries.reduce(0) { $0 + selectedDataType.getValue(from: $1) }
            return (date: date, total: sum)
        }.sorted { $0.date < $1.date }
    }

    // 计算Y轴最大值
    private func yAxisMax() -> Double {
        let maxVal = aggregatedData().map { $0.total }.max() ?? 1
        return max(maxVal, 1) * 1.1
    }

    // 计算Y轴最小值
    private func yAxisMin() -> Double {
        let values = aggregatedData().map { $0.total }
        let maxVal = values.max() ?? 1

        if values.allSatisfy({ $0 == 0 }) {
            return -0.1
        } else {
            return max(0, maxVal * 0.95) * -0.1
        }
    }

2. 实现平滑切换的图表配置

修改Chart组件,动态适配X轴范围、刻度样式,并添加平滑动画:

var body: some View {
        NavigationView {
            ZStack {
                LinearGradient(
                    gradient: Gradient(stops: [
                        .init(color: Color(uiColor: .systemBackground), location: 0.1),
                        .init(color: selectedDataType.tintColor, location: 0.7),
                        .init(color: Color(uiColor: .systemBackground), location: 1.2)
                    ]),
                    startPoint: .top,
                    endPoint: .bottom
                )
                .animation(.easeInOut(duration: 0.6), value: selectedDataType)
                .mask(
                    LinearGradient(
                        gradient: Gradient(stops: [
                            .init(color: Color.black.opacity(0.3), location: -0.5),
                            .init(color: Color.black.opacity(0.5), location: 0.5),
                            .init(color: Color.black.opacity(0.3), location: 1.5)
                        ]),
                        startPoint: .leading,
                        endPoint: .trailing
                    )
                )
                .edgesIgnoringSafeArea(.all)
                
                VStack {
                    Text("图表")
                        .foregroundColor(.primary)
                        .opacity(0.5)
                        .fontWeight(.bold)
                        .padding()
                    
                    Chart(aggregatedData(), id: \.date) { item in
                        LineMark(
                            x: .value("日期", item.date),
                            y: .value("总计", item.total)
                        )
                        .foregroundStyle(selectedDataType.tintColor)
                        .lineStyle(StrokeStyle(lineWidth: 8, lineCap: .round))
                        .interpolationMethod(.monotone)
                    }
                    .chartScrollableAxes(.horizontal)
                    // 动态设置X轴时间范围
                    .chartXScale(domain: {
                        let calendar = Calendar.current
                        let endDate = Date()
                        let startDate = calendar.date(byAdding: .day, value: -selectedTimeRange.dayCount, to: endDate)!
                        return startDate...endDate
                    }())
                    .chartYScale(domain: yAxisMin()...yAxisMax())
                    .frame(height: 200)
                    .chartXAxis {
                        AxisMarks(position: .bottom, values: .stride(by: selectedTimeRange == .threeMonths ? .weekOfYear : .month)) { value in
                            if let date = value.as(Date.self) {
                                // 动态调整日期显示格式
                                let dateFormat: Date.FormatStyle = selectedTimeRange == .threeMonths
                                    ? .dateTime.weekday(.abbreviated).day()
                                    : .dateTime.month(.abbreviated)
                                AxisValueLabel(format: dateFormat)
                            }
                            AxisTick()
                            AxisGridLine()
                        }
                    }
                    .chartYAxis {}
                    // 添加平滑切换动画
                    .animation(.easeInOut(duration: 0.6), value: selectedTimeRange)
                    .animation(.timingCurve(0.2, 0.5, 0, 1, duration: 0.8), value: selectedDataType)
                    
                    // 数据类型选择器
                    Picker("数据类型", selection: $selectedDataType) {
                        Image(systemName: "dollarsign").tag(DataType.expense)
                        Image(systemName: "fuelpump.fill").tag(DataType.fuel)
                        Image(systemName: "road.lanes").tag(DataType.mileage)
                        Image(systemName: "bubbles.and.sparkles.fill").tag(DataType.carWash)
                    }
                    .pickerStyle(.segmented)
                    .padding()
                    
                    // 时间范围选择器
                    Picker("时间范围", selection: $selectedTimeRange) {
                        Text("3个月").tag(TimeRange.threeMonths)
                        Text("9个月").tag(TimeRange.nineMonths)
                    }
                    .pickerStyle(.segmented)
                    .padding(.horizontal, 80)
                }
                .background(.thinMaterial)
                .cornerRadius(20)
                .shadow(color: .black.opacity(0.1), radius: 10, x: 0, y: 10)
                .padding([.leading, .bottom, .trailing])
                
                NavigationBarV(backgroundColor: Color(UIColor.bgGradient1), title: "图表", back: "汇总", contentHasScrolled: $contentHasScrolled)
                
                VStack {
                    Spacer()
                    HStack {
                        Spacer()
                        BottomButtonV(bottomButton: BottomButtonM(icon: "plus", color: Color.primary)) {
                            isSheetPresented.toggle()
                        }
                    }
                    .padding()
                }
            }
            .sheet(isPresented: $isSheetPresented) {
                NewEntryV(dataStore: dataStore) {
                    isSheetPresented = false
                }
            }
        }
    }
}

3. 关键优化说明

  • 数据聚合:通过字典分组+求和,确保不同时间粒度下展示的是汇总数据,而非原始单条记录
  • 动态X轴:使用chartXScale明确指定时间范围,配合动画实现平滑缩放效果
  • 刻度适配:根据时间范围自动切换刻度步长和日期格式,3个月视图显示周+日,9个月视图仅显示月份
  • 状态拆分:将原有的单一type变量拆分为数据类型和时间范围两个独立状态,避免逻辑混淆

内容的提问来源于stack exchange,提问作者Dawid

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 13:35:15