news 2026/5/1 3:04:22

【swiftUI】实现智能可收缩日历(单行/全月切换)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【swiftUI】实现智能可收缩日历(单行/全月切换)

一、 核心特性

  1. 智能显示模式收起状态--仅显示当前日期所在的整周(7天);展开状态--显示完整月份的日历网格;平滑的动画过渡效果

  2. 数据一致性:始终显示当前月份的数据;收起时自动定位到当前周;日期选择后自动更新显示

  3. 交互体验:点击日历任意区域切换展开/收起;日期点击选中效果;月份切换导航;"今天"按钮快速定位

  4. 视觉设计:现代化卡片设计;清晰的视觉层次;响应式布局

二、代码实现

1. 日期cell样式

// MARK: - 日期单元格组件 struct DateCell: View { let date: Date let isSelected: Bool let isToday: Bool let displayMode: DisplayMode enum DisplayMode { case compact // 收起状态 case expanded // 展开状态 } @Environment(\.colorScheme) var colorScheme private var isCurrentMonth: Bool { Calendar.current.isDate(date, equalTo: Date(), toGranularity: .month) } private var isBlankDay: Bool { !Calendar.current.isDate(date, equalTo: Date(), toGranularity: .month) } var body: some View { VStack(spacing: 2) { // 日期数字 Text("\(dayNumber)") .font(fontForMode()) .fontWeight(fontWeight()) .foregroundColor(textColor()) .frame(width: cellSize(), height: cellSize()) .background(backgroundCircle) } } private var dayNumber: Int { Calendar.current.component(.day, from: date) } private var weekdaySymbol: String { let formatter = DateFormatter() formatter.locale = Locale(identifier: "zh_CN") formatter.dateFormat = "EEE" return String(formatter.string(from: date).prefix(1)) } private func cellSize() -> CGFloat { displayMode == .compact ? 32 : 28 } private func fontForMode() -> Font { displayMode == .compact ? .callout : .caption } private func fontWeight() -> Font.Weight { isToday ? .bold : .regular } private func textColor() -> Color { if isSelected { return .white } else if isToday { return .orange } else if isBlankDay { return .gray.opacity(0.3) } else { return colorScheme == .dark ? .white : .black } } private var backgroundCircle: some View { Group { if isSelected { Circle() .fill(Color.orange) .shadow(color: .yellow.opacity(0.3), radius: 3, x: 0, y: 2) } else if isToday { Circle() .stroke(Color.orange, lineWidth: 1.5) } else if isBlankDay { Circle() .fill(Color.clear) } else { Circle() .fill(Color.clear) } } } }

2. 基础款样式

struct CalendarView: View { @State private var currentDate = Date() @State private var selectedDate = Date() @State private var currentMonth = 0 let columns = Array(repeating: GridItem(.flexible()), count: 7) var body: some View { VStack(spacing: 20) { // 头部:月份和年份 headerView // 星期标题 weekdaysView // 日期网格 datesGrid Spacer() } .padding() } // MARK: - 头部视图 var headerView: some View { HStack { Button(action: { withAnimation { currentMonth -= 1 } }) { Image(systemName: "chevron.left") .font(.title3) } .buttonStyle(CircleIconButtonStyle(size: 30, backgroundColor: .orange)) Spacer() Text(monthYearString()) .font(.title2.bold()) Spacer() Button(action: { withAnimation { currentMonth += 1 } }) { Image(systemName: "chevron.right") .font(.title3) } .buttonStyle(CircleIconButtonStyle(size: 30, backgroundColor: .orange)) } .padding(.horizontal) } // MARK: - 星期标题 var weekdaysView: some View { HStack(spacing: 0) { ForEach(["日", "一", "二", "三", "四", "五", "六"], id: \.self) { day in Text(day) .font(.callout) .fontWeight(.semibold) .frame(maxWidth: .infinity) .foregroundColor(.gray) } } } // MARK: - 日期网格 var datesGrid: some View { LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 7), spacing: 12) { ForEach(getMonthDates(), id: \.self) { date in DateCell( date: date, isSelected: Calendar.current.isDate(date, inSameDayAs: selectedDate), isToday: Calendar.current.isDateInToday(date), displayMode: .compact // 月模式 ) .frame(height: 36) .onTapGesture { withAnimation(.easeInOut(duration: 0.2)) { selectedDate = date } } } } } } // MARK: - 数据方法扩展 extension CalendarView { // func monthYearString() -> String { let formatter = DateFormatter() formatter.dateFormat = "yyyy年 M月" guard let date = Calendar.current.date(byAdding: .month, value: currentMonth, to: Date()) else { return "" } return formatter.string(from: date) } // 获取当前月份的日期 private func getMonthDates() -> [Date] { let calendar = Calendar.current // 获取当前显示的月份 guard let currentMonthDate = calendar.date(byAdding: .month, value: currentMonth, to: Date()) else { return [] } // 获取月份的第一天 guard let startOfMonth = calendar.date(from: calendar.dateComponents([.year, .month], from: currentMonthDate)) else { return [] } // 获取月份的天数 guard let range = calendar.range(of: .day, in: .month, for: startOfMonth) else { return [] } // 生成日期数组 var dates: [Date] = [] for day in range { if let date = calendar.date(byAdding: .day, value: day - 1, to: startOfMonth) { dates.append(date) } } // 添加前面空白日期 let firstWeekday = calendar.component(.weekday, from: dates.first!) for _ in 1..<firstWeekday { dates.insert(calendar.date(byAdding: .day, value: -1, to: dates.first!)!, at: 0) } return dates } }

2. 添加收缩功能

struct SmartCollapsibleCalendar: View { @State private var isExpanded = false @State private var selectedDate = Date() @State private var currentMonth = 0 // 高度配置 private let collapsedHeight: CGFloat = 130 // 单行高度 private let expandedHeight: CGFloat = 300 // 完整月份高度 var body: some View { VStack { // 容器 calendarContainer .frame(height: isExpanded ? expandedHeight : collapsedHeight) .contentShape(Rectangle()) .onTapGesture { withAnimation(.spring(response: 0.35, dampingFraction: 0.8)) { isExpanded.toggle() } } } .background( RoundedRectangle(cornerRadius: 16) .fill(Color(.systemBackground)) .shadow(color: .black.opacity(0.1), radius: 12, x: 0, y: 4) ) .padding(.horizontal) } // MARK: - 日历容器 private var calendarContainer: some View { VStack(spacing: 0) { // 顶部导航栏 calendarNavigationBar // 星期标题(始终显示) weekdaysHeader .padding(.vertical, 8) // // 日期内容区域 calendarContent } .padding(.horizontal, 16) } // MARK: - 日历导航栏 private var calendarNavigationBar: some View { HStack { // 月份标题 Text(monthYearString()) .font(.headline) .fontWeight(.semibold) .foregroundColor(.primary) Spacer() // 今天按钮 todayButton // 展开/收起指示器 expandIndicator } .padding(.top, 16) .padding(.bottom, 12) } // MARK: - 今天按钮 private var todayButton: some View { Button(action: { withAnimation { selectedDate = Date() currentMonth = 0 // 重置到当前月 } }) { Text("今天") .font(.caption.bold()) .foregroundColor(.white) .padding(.horizontal, 12) .padding(.vertical, 6) .background(Color.blue) .cornerRadius(8) } } // MARK: - 展开指示器 private var expandIndicator: some View { Image(systemName: "chevron.down") .font(.caption.bold()) .foregroundColor(.blue) .rotationEffect(.degrees(isExpanded ? 180 : 0)) .animation(.spring(), value: isExpanded) .padding(.leading, 8) } // MARK: - 星期标题 private var weekdaysHeader: some View { HStack(spacing: 0) { ForEach(["日", "一", "二", "三", "四", "五", "六"], id: \.self) { day in Text(day) .font(.caption) .fontWeight(.medium) .foregroundColor(.secondary) .frame(maxWidth: .infinity) } } } // MARK: - 日历内容区域 private var calendarContent: some View { Group { if isExpanded { // 展开状态:显示完整月份 fullMonthView .transition(.opacity.combined(with: .scale(scale: 0.95))) } else { // 收起状态:只显示当前周 singleWeekView .transition(.opacity.combined(with: .scale(scale: 0.95))) } } .animation(.spring(response: 0.3, dampingFraction: 0.85), value: isExpanded) } // MARK: - 单周视图(收起状态) private var singleWeekView: some View { HStack { ForEach(getCurrentWeekDates(), id: \.self) { date in DateCell( date: date, isSelected: Calendar.current.isDate(date, inSameDayAs: selectedDate), isToday: Calendar.current.isDateInToday(date), displayMode: .compact ) .frame(maxWidth: .infinity) .onTapGesture { withAnimation(.easeInOut(duration: 0.2)) { selectedDate = date } } } } } // MARK: - 完整月份视图(展开状态) private var fullMonthView: some View { VStack(spacing: 12) { // 月份切换导航 monthNavigation // 月份网格 monthGrid } } // MARK: - 月份切换导航 private var monthNavigation: some View { HStack { Button(action: { withAnimation(.spring()) { currentMonth -= 1 } }) { Image(systemName: "chevron.left") .font(.caption.bold()) .foregroundColor(.orange) .frame(width: 28, height: 28) .background(Color.yellow.opacity(0.1)) .clipShape(Circle()) } Spacer() Text(monthYearString()) .font(.title3.bold()) .foregroundColor(.primary) Spacer() Button(action: { withAnimation(.spring()) { currentMonth += 1 } }) { Image(systemName: "chevron.right") .font(.caption.bold()) .foregroundColor(.orange) .frame(width: 28, height: 28) .background(Color.yellow.opacity(0.1)) .clipShape(Circle()) } } .padding(.horizontal, 4) } // MARK: - 月份网格 private var monthGrid: some View { LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 7), spacing: 8) { ForEach(getMonthDates(), id: \.self) { date in DateCell( date: date, isSelected: Calendar.current.isDate(date, inSameDayAs: selectedDate), isToday: Calendar.current.isDateInToday(date), displayMode: .expanded ) .frame(height: 32) .onTapGesture { withAnimation(.easeInOut(duration: 0.2)) { selectedDate = date } } } } } } // MARK: - 数据工具方法 extension SmartCollapsibleCalendar { // 获取当前周的日期 private func getCurrentWeekDates() -> [Date] { let calendar = Calendar.current let startOfWeek = calendar.date(from: calendar.dateComponents([.yearForWeekOfYear, .weekOfYear], from: selectedDate))! var weekDates: [Date] = [] for i in 0..<7 { if let date = calendar.date(byAdding: .day, value: i, to: startOfWeek) { weekDates.append(date) } } return weekDates } // 获取月份年份字符串 private func monthYearString() -> String { let calendar = Calendar.current guard let date = calendar.date(byAdding: .month, value: currentMonth, to: Date()) else { return "" } let formatter = DateFormatter() formatter.locale = Locale(identifier: "zh_CN") formatter.dateFormat = "yyyy年M月" return formatter.string(from: date) } // 获取当前月份的日期 private func getMonthDates() -> [Date] { let calendar = Calendar.current // 获取当前显示的月份 guard let currentMonthDate = calendar.date(byAdding: .month, value: currentMonth, to: Date()) else { return [] } // 获取月份的第一天 guard let startOfMonth = calendar.date(from: calendar.dateComponents([.year, .month], from: currentMonthDate)) else { return [] } // 获取月份的天数 guard let range = calendar.range(of: .day, in: .month, for: startOfMonth) else { return [] } // 生成日期数组 var dates: [Date] = [] for day in range { if let date = calendar.date(byAdding: .day, value: day - 1, to: startOfMonth) { dates.append(date) } } // 添加前面空白日期 let firstWeekday = calendar.component(.weekday, from: dates.first!) for _ in 1..<firstWeekday { dates.insert(calendar.date(byAdding: .day, value: -1, to: dates.first!)!, at: 0) } return dates } // 获取日期在月份中的周索引 private func getWeekIndex(for date: Date, in monthDates: [Date]) -> Int { let calendar = Calendar.current let weekOfMonth = calendar.component(.weekOfMonth, from: date) // 处理月份开始的空白日期 let firstRealDateIndex = monthDates.firstIndex { calendar.isDate($0, equalTo: monthDates[0], toGranularity: .month) } ?? 0 // 计算调整后的周索引 let adjustedWeek = weekOfMonth - 1 // 确保索引在有效范围内 let weekStartIndex = adjustedWeek * 7 return min(max(0, adjustedWeek), (monthDates.count / 7) - 1) } }
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/1 1:33:04

MemOS开源框架实战:构建基于Graph的记忆图谱,让AI具备长期记忆能力

MemOS是一款基于Graph的记忆开源项目&#xff0c;通过TreeTextMemory实现结构化记忆存储与关联。本文详细介绍了如何在LangChain1.x框架中集成MemOS的记忆图谱能力&#xff0c;包括构建带记忆的ChatBot、体验记忆的自动重组功能&#xff0c;以及通过Middleware机制实现无侵入式…

作者头像 李华
网站建设 2026/4/27 11:35:55

windows QT项目

一、1、2、在纯linux环境下用QTCreator这个IDE去编项目&#xff1b;但是如果在windows上编写qt项目&#xff0c;VS这个IDE还是比QTCreator要强大很多&#xff0c;而且VS编写的QT项目&#xff0c;仍然可以移植到Linux等平台上&#xff0c;不影响跨平台特性&#xff01;&#xff…

作者头像 李华
网站建设 2026/4/22 23:25:22

‌新闻事件分析:社交媒体数据验证测试案例

为什么社交媒体数据正在重塑测试边界‌在2026年&#xff0c;软件测试的战场已不再局限于API响应码、数据库事务一致性或UI布局像素偏差。‌社交媒体数据‌&#xff0c;作为全球最庞大、最动态、最不可控的非结构化输入源&#xff0c;正成为系统鲁棒性验证的“终极压力测试场”。…

作者头像 李华
网站建设 2026/4/20 21:47:04

vue2基础--实现下拉框选择筛选

下拉框筛选常用于搜索筛选出列表中的数据1.代码<el-selectv-model"type"clearableplaceholder"类型" ><el-optionv-for"item in typeOptions":key"item.value":label"item.label":value"item.value&quo…

作者头像 李华
网站建设 2026/4/29 17:00:32

2026年生物计算开发者伦理自查清单

第一章 生物计算测试的特殊性认知 1.1 生命数据敏感性分级 风险维度 测试关注要点 检测工具示例 基因数据 脱敏后仍可逆向识别风险 HelixSec渗透测试套件 神经接口信号 脑波模式唯一性验证 NeuroGuard仿真平台 生物特征标识 3D打印攻击向量测试 BioReplica漏洞扫描…

作者头像 李华
网站建设 2026/4/28 23:56:03

情感驱动:星际团队如何建立“光年信任”?

信任的星际挑战与情感驱动新范式 在软件测试领域&#xff0c;分布式团队&#xff08;常被喻为“星际团队”&#xff09;已成为常态&#xff0c;成员跨越时区协作&#xff0c;却常陷入“本地可复现-线上必失败”的信任危机。这种“光年信任”缺失不仅导致缺陷漏测、项目延误&am…

作者头像 李华