SwiftUI实现可点击横向日历:选中日期联动下方卡片
问题描述
需要实现如下布局效果:
- 顶部固定展示7天日期,当日始终处于中间位置
- 支持点击任意日期,点击后下方卡片切换为对应日期的描述内容,实现日期与内容的关联
目前已完成基础日期布局,但不清楚如何通过@State或类似状态变量实现点击交互,之前尝试在ZStack中使用@State未成功。现有代码如下:
import SwiftUI struct DayPlanView: View { @State var monthString: String = "Not Set" let calendar = Calendar.current var body: some View { let dates = getWeek() VStack { // MARK: Change Date() to some identifier of selected date. HStack { Text("\(getDayNumber(date: Date()))") .font(.title2) .fontWeight(.semibold) Text("of") .font(.title2) .fontWeight(.semibold) Text(getMonth(date: Date())) .font(.title2) .fontWeight(.semibold) } HStack { ForEach(dates, id: \.self) { day in VStack { let dayNumber = getDayNumber(date: day) if dayNumber == getDayNumber(date: Date()) { ZStack { Circle() .fill(.blue) .scaleEffect(1.3) Text("\(getDayNumber(date: day))") .font(.title) .fontWeight(.semibold) .foregroundColor(.white) } } else { Text("\(getDayNumber(date: day))") .font(.title) } } .frame(width: getWidth() / 8, height: getHeight() / 20) .padding(.horizontal, -3) } } .frame(width: getWidth()) Spacer() } .padding(.vertical, getHeight() / 20) } func getMonth(date: Date) -> String { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "LLLL" return dateFormatter.string(from: date) } func getDayShort(date: Date) -> String { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "E" return dateFormatter.string(from: date) } func getDayNumber(date: Date) -> Int { let calendar = Calendar.current let components = calendar.dateComponents([.day], from: date) return components.day ?? 0 } func getWeek() -> [Date] { let currentDate = Date() let calendar = Calendar.current let dayOfWeek = calendar.component(.weekday, from: currentDate) let range = calendar.range(of: .day, in: .month, for: currentDate)! let daysMonth = (range.lowerBound - 1 ..< range.lowerBound + 6) .compactMap { calendar.date(byAdding: .day, value: $0 - dayOfWeek, to: currentDate) } return daysMonth } } struct DayPlanView_Previews: PreviewProvider { static var previews: some View { DayPlanView() } }
当前界面截图:
解决方案
要实现点击日期切换内容的功能,核心是用@State变量追踪用户选中的日期,通过状态变化驱动UI更新。以下是修改后的完整代码,关键改动已标注:
import SwiftUI struct DayPlanView: View { // 追踪选中的日期,默认设为当前日期 @State private var selectedDate: Date = Date() @State var monthString: String = "Not Set" let calendar = Calendar.current var body: some View { let dates = getWeek() VStack { // 显示选中日期的日和月,替代原固定的当前日期 HStack { Text("\(getDayNumber(date: selectedDate))") .font(.title2) .fontWeight(.semibold) Text("of") .font(.title2) .fontWeight(.semibold) Text(getMonth(date: selectedDate)) .font(.title2) .fontWeight(.semibold) } HStack { ForEach(dates, id: \.self) { day in VStack { let dayNumber = getDayNumber(date: day) // 同时判断是否为当日或选中日期,确保两种状态都高亮 let isHighlighted = isSameDay(day, Date()) || isSameDay(day, selectedDate) if isHighlighted { ZStack { Circle() .fill(.blue) .scaleEffect(1.3) Text("\(dayNumber)") .font(.title) .fontWeight(.semibold) .foregroundColor(.white) } } else { Text("\(dayNumber)") .font(.title) } } .frame(width: UIScreen.main.bounds.width / 8, height: UIScreen.main.bounds.height / 20) .padding(.horizontal, -3) // 添加点击手势,更新选中日期 .onTapGesture { selectedDate = day } } } .frame(width: UIScreen.main.bounds.width) // 下方内容区域:根据选中日期展示对应描述 VStack(spacing: 16) { Text("当日计划") .font(.title) .fontWeight(.bold) Text(getPlanForDate(selectedDate)) .font(.body) .padding() .background(Color.gray.opacity(0.1)) .cornerRadius(12) } .padding(.horizontal) Spacer() } .padding(.vertical, UIScreen.main.bounds.height / 20) } // 判断两个日期是否为同一天(避免时间部分干扰判断) private func isSameDay(_ date1: Date, _ date2: Date) -> Bool { return calendar.isDate(date1, inSameDayAs: date2) } // 模拟根据日期返回对应计划的方法,实际可替换为你的数据源 private func getPlanForDate(_ date: Date) -> String { let dayNumber = getDayNumber(date: date) return "今天是\(getMonth(date: date))\(dayNumber)日,你的计划是:完成SwiftUI布局优化,整理项目文档,下午参加团队会议。" } func getMonth(date: Date) -> String { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "LLLL" dateFormatter.locale = Locale(identifier: "zh_CN") // 显示中文月份 return dateFormatter.string(from: date) } func getDayShort(date: Date) -> String { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "E" dateFormatter.locale = Locale(identifier: "zh_CN") // 显示中文星期 return dateFormatter.string(from: date) } func getDayNumber(date: Date) -> Int { let components = calendar.dateComponents([.day], from: date) return components.day ?? 0 } func getWeek() -> [Date] { let currentDate = Date() // 确保当日位于7天列表的中间位置(第4位) let startOfWeek = calendar.date(from: calendar.dateComponents([.yearForWeekOfYear, .weekOfYear], from: currentDate))! var days = [Date]() // 从当前周的前3天到后3天,共7天 for i in -3...3 { if let date = calendar.date(byAdding: .day, value: i, to: startOfWeek) { days.append(date) } } return days } } struct DayPlanView_Previews: PreviewProvider { static var previews: some View { DayPlanView() } }
核心改动说明
- 状态变量追踪选中日期:新增
@State private var selectedDate,用于记录用户点击的日期,状态变化会自动触发UI更新 - 添加点击交互:给每个日期的容器添加
.onTapGesture,点击时更新selectedDate - UI与状态关联:
- 顶部标题改为展示
selectedDate的信息 - 日期高亮逻辑同时覆盖「当日」和「选中日期」,用户点击后对应日期会显示蓝色圆圈
- 顶部标题改为展示
- 内容切换实现:新增下方内容区域,通过
getPlanForDate方法根据选中日期返回对应描述,实现日期与内容的绑定 - 日期逻辑优化:修改
getWeek方法确保当日始终在7天列表中间;新增isSameDay方法,避免因日期的时间部分导致判断错误 - 本地化适配:给日期格式化器添加中文区域设置,显示中文月份和星期
内容的提问来源于stack exchange,提问作者Anna
相关产品推荐
相关产品推荐

