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

SwiftUI中实现周步数数据左右滑动切换功能的技术问询

周步数数据切换实现方案

不需要使用TabView和Index,通过状态变量控制周偏移量、改造数据获取函数、结合手势和按钮即可实现需求,以下是具体修改步骤:

1. 核心逻辑调整

  • 添加currentWeekOffset状态变量,记录当前展示的周与最近一周的偏移(0=最近7天,-1=前一周,-2=再前一周…)
  • 改造fetchWeeklyStepData函数,接受偏移参数计算目标周的日期范围
  • 区分两种滑动交互:短距离滑动选择单日数据,长距离滑动切换周数据;同时添加按钮切换入口

2. 修改WeeklyStepsStats视图

struct WeeklyStepsStats: View {
    @State private var stepData: [StepData] = []
    @State private var currentWeekOffset = 0 // 0=最近7天,负数表示往前推的周数

    var body: some View {
        VStack(spacing: 20) {
            // 周切换按钮组
            HStack {
                Button("上一周") {
                    currentWeekOffset -= 1
                    fetchWeeklyStepData(offset: currentWeekOffset)
                }
                .disabled(currentWeekOffset < -10) // 限制最多查询10周前数据,可按需调整

                Spacer()

                Button("下一周") {
                    currentWeekOffset += 1
                    if currentWeekOffset > 0 { // 不能超过当前周
                        currentWeekOffset = 0
                    }
                    fetchWeeklyStepData(offset: currentWeekOffset)
                }
                .disabled(currentWeekOffset == 0)
            }
            .padding(.horizontal)

            if stepData.isEmpty {
                Text("No health data available")
                    .padding()
            } else {
                VStack {
                    WeeklyBarGraph(stepData: stepData, onWeekSwipe: { direction in
                        switch direction {
                        case .left:
                            if currentWeekOffset > -10 {
                                currentWeekOffset -= 1
                                fetchWeeklyStepData(offset: currentWeekOffset)
                            }
                        case .right:
                            if currentWeekOffset < 0 {
                                currentWeekOffset += 1
                                fetchWeeklyStepData(offset: currentWeekOffset)
                            }
                        }
                    })
                }
                .background(
                    RoundedRectangle(cornerRadius: 20)
                        .fill(Color.white)
                )
                .padding()
            }
        }
        .onAppear {
            fetchWeeklyStepData(offset: currentWeekOffset)
        }
    }

    func fetchWeeklyStepData(offset: Int = 0) {
        guard let stepCountType = HKQuantityType.quantityType(forIdentifier: .stepCount) else {
            return
        }

        let calendar = Calendar.current
        let startOfToday = calendar.startOfDay(for: Date())

        // 根据偏移量计算目标周的结束日期
        guard let targetWeekEndDate = calendar.date(byAdding: .day, value: offset * 7, to: startOfToday) else {
            return
        }
        // 目标周的开始日期为结束日期往前推7天
        guard let targetWeekStartDate = calendar.date(byAdding: .day, value: -7, to: targetWeekEndDate) else {
            return
        }

        let predicate = HKQuery.predicateForSamples(withStart: targetWeekStartDate, end: targetWeekEndDate, options: .strictStartDate)

        let query = HKStatisticsCollectionQuery(quantityType: stepCountType,
                                                quantitySamplePredicate: predicate,
                                                options: .cumulativeSum,
                                                anchorDate: targetWeekStartDate,
                                                intervalComponents: DateComponents(day: 1))

        query.initialResultsHandler = { query, results, error in
            guard let results = results else {
                return
            }

            var newData: [StepData] = []

            results.enumerateStatistics(from: targetWeekStartDate, to: targetWeekEndDate) { statistics, _ in
                if let sumQuantity = statistics.sumQuantity() {
                    let date = statistics.startDate
                    let stepCount = sumQuantity.doubleValue(for: HKUnit.count())
                    let data = StepData(id: UUID().uuidString, numberOfSteps: stepCount, date: date)
                    newData.append(data)
                }
            }

            DispatchQueue.main.async {
                stepData = newData
            }
        }

        HKHealthStore().execute(query)
    }
}

3. 修改WeeklyBarGraph视图

首先定义滑动方向枚举:

enum SwipeDirection {
    case left, right
}

然后更新视图,添加周切换回调并区分手势:

struct WeeklyBarGraph: View {
    var stepData: [StepData]
    var onWeekSwipe: ((SwipeDirection) -> Void)? // 周切换回调
    @GestureState var isDragging: Bool = false
    @State var offset: CGFloat = 0
    @State var currentDataID: String = ""
    @State var isTapped: Bool = false
    @State var showAverageText: Bool = true
    
    private let minSwipeDistance: CGFloat = 50 // 触发周切换的最小滑动距离

    var body: some View {
        VStack {
            VStack {
                Text("Weekly Summary")
                    .font(.title2)
                    .fontWeight(.semibold)
                Text(getDateRangeText())
                    .font(.subheadline)
                    .foregroundColor(.gray)
            }
            .padding(.top)

            VStack(spacing: 20) {
                HStack(spacing: 10) {
                    ForEach(stepData) { data in
                        CardView(data: data)
                            .onTapGesture {
                                self.currentDataID = data.id
                                self.showAverageText = false
                            }
                    }
                }
                .animation(.easeOut, value: isDragging)
                .gesture(
                    DragGesture()
                        .updating($isDragging) { value, state, _ in
                            state = true
                        }
                        .onChanged { value in
                            offset = value.translation.width
                            // 计算当前选中的单日数据
                            let draggingSpace = UIScreen.main.bounds.width - 60
                            let eachBlock = draggingSpace / CGFloat(stepData.count)
                            let temp = Int(-offset / eachBlock)
                            let index = max(min(temp, stepData.count - 1), 0)
                            self.currentDataID = stepData[index].id
                            self.showAverageText = false
                        }
                        .onEnded { value in
                            withAnimation {
                                offset = .zero
                                // 判断是否触发周切换
                                if abs(value.translation.width) > minSwipeDistance {
                                    let direction = value.translation.width > 0 ? SwipeDirection.right : SwipeDirection.left
                                    onWeekSwipe?(direction)
                                    currentDataID = ""
                                    showAverageText = true
                                } else {
                                    // 滑动距离不足,保留单日选中状态
                                    if currentDataID.isEmpty {
                                        showAverageText = true
                                    }
                                }
                            }
                        }
                )

                RoundedRectangle(cornerRadius: 10)
                    .fill(Color.gray.opacity(0.1))
                    .frame(height: 80)
                    .overlay(
                        VStack(spacing: 8) {
                            if showAverageText {
                                VStack {
                                    Text("Average")
                                        .foregroundColor(.black)
                                        .font(.system(size: UIScreen.main.bounds.size.width * 0.065))
                                        .fontWeight(.semibold)
                                    
                                    Text("\(getAverageStepsPerDay())")
                                        .foregroundColor(.black)
                                        .font(.system(size: UIScreen.main.bounds.size.width * 0.065))
                                }
                            } else {
                                if let selectedData = stepData.first(where: { $0.id == currentDataID }) {
                                    VStack(spacing: 8) {
                                        Text(getFormattedDate(selectedData.date))
                                            .font(.subheadline)
                                            .foregroundColor(.gray)
                                        Text("\(Int(selectedData.numberOfSteps))")
                                            .font(.title)
                                            .fontWeight(.semibold)
                                    }
                                }
                            }
                        }
                    )
            }
            .padding(.horizontal)
            .padding(.bottom)
        }
    }
    
    func getAverageStepsPerDay() -> Int {
        let totalSteps = stepData.reduce(0) { $0 + $1.numberOfSteps }
        let averageSteps = Double(totalSteps) / Double(stepData.count)
        return Int(averageSteps)
    }
    
    func getFormattedDate(_ date: Date) -> String {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "E, d"
        return dateFormatter.string(from: date)
    }
    
    func getDateRangeText() -> String {
        guard let startDate = stepData.first?.date,
              let endDate = stepData.last?.date else {
            return ""
        }
        
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "MMM d"
        
        let startDateText = dateFormatter.string(from: startDate)
        let endDateText = dateFormatter.string(from: endDate)
        
        return "\(startDateText) - \(endDateText)"
    }
    
    @ViewBuilder
    func CardView(data: StepData) -> some View {
        VStack(spacing: 8) {
            GeometryReader { proxy in
                let size = proxy.size
                let maxSteps = getMaxNumberOfSteps()
                let barHeight = maxSteps > 0 ? (data.numberOfSteps / maxSteps) * size.height : 0
                
                RoundedRectangle(cornerRadius: 6)
                    .fill(.blue)
                    .opacity((isDragging || isTapped) ? (currentDataID == data.id ? 1 : 0.35) : 1)
                    .frame(height: barHeight)
                    .frame(maxHeight: .infinity, alignment: .bottom)
            }
            .frame(width: 30, height: 120)
            
            Text(data.date.dayOfWeekLetter())
                .font(.callout)
                .foregroundColor((isDragging || isTapped) && currentDataID == data.id ? .blue : .gray)
        }
    }
    
    func getMaxNumberOfSteps() -> Double {
        stepData.max { $0.numberOfSteps < $1.numberOfSteps }?.numberOfSteps ?? 0
    }
}

// 补充Date扩展(如果原有代码中没有)
extension Date {
    func dayOfWeekLetter() -> String {
        let formatter = DateFormatter()
        formatter.dateFormat = "EEEEE" // 单个字母的星期缩写(如M、T、W)
        return formatter.string(from: self)
    }
}

4. StepData结构保持不变

struct StepData: Identifiable {
    let id: String
    let numberOfSteps: Double
    let date: Date
}

关键说明

  • 周偏移计算:通过currentWeekOffset * 7计算目标周的结束日期,再往前推7天得到开始日期,确保每次查询都是完整的7天数据
  • 手势区分:设置50pt的最小滑动距离,避免用户选择单日时误触发周切换
  • 边界控制:限制最多查询10周前的数据,同时禁止切换到未来的周
  • 状态同步:周切换后自动重置单日选中状态,回到显示周平均的初始状态

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.18 02:38:13