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
相关产品推荐
相关产品推荐

