news 2026/4/15 18:54:37

【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/4/12 17:31:22

气候事件应用:云原生系统弹性测试设计

1. 总述&#xff1a;云原生系统与气候事件弹性测试概述 随着气候变化加剧&#xff0c;极端天气事件&#xff08;如洪水、飓风、热浪&#xff09;频发&#xff0c;对数字化基础设施构成严峻挑战。云原生系统&#xff08;Cloud-Native Systems&#xff09;&#xff0c;基于微服务…

作者头像 李华
网站建设 2026/4/11 18:56:14

‌金融波动场景下的交易流程稳定性测试强化

‌一、背景&#xff1a;金融波动如何重塑测试范式‌ 金融市场的瞬时波动——如美股闪崩、人民币汇率跳水、加密资产暴跌——正从“偶发风险”演变为“常态压力源”。2023年中信证券因UPS断电导致交易系统中断19分钟&#xff0c;2025年支付宝消息库局部故障引发支付卡顿&#x…

作者头像 李华
网站建设 2026/4/9 16:28:20

CAD加密软件哪个好?2026精选5款CAD加密软件,千万别错过

你的核心图纸&#xff0c;可能正被竞争对手“免费预览”。要知道CAD图纸作为企业的设计命脉&#xff0c;一旦泄露&#xff0c;损失动辄百万。设计师熬夜赶稿&#xff0c;老板投入重金研发&#xff0c;却因安全漏洞让设计成果付诸东流&#xff0c;这会让多少老板痛彻心扉。别担心…

作者头像 李华
网站建设 2026/4/10 17:12:34

5654645

5464545

作者头像 李华
网站建设 2026/4/12 4:29:35

Python系列基础教程(二)Python基础数据类型与常用运算符

一、课程前言 数据是程序的核心处理对象&#xff0c;不同数据对应不同操作规则。例如数字可进行数学计算&#xff0c;文本无法直接参与除法运算。本节将系统讲解Python基础数据类型、类型判断与转换方法&#xff0c;以及算术、赋值、字符串相关运算符&#xff0c;同时引入输入函…

作者头像 李华
网站建设 2026/4/11 23:09:09

基于大数据的通化市人口老龄化分析平台开题报告

基于大数据的通化市人口老龄化分析平台开题报告 一、选题背景与意义 &#xff08;一&#xff09;选题背景 随着我国社会经济的持续发展、医疗保障体系的不断完善以及人口生育政策的调整&#xff0c;人口老龄化已成为不可逆转的社会发展趋势&#xff0c;对社会结构、经济发展、公…

作者头像 李华