news 2026/5/23 17:38:29

【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/23 7:10:39

华为激活组织的“五大引擎”与“四驾马车”

在瞬息万变的商业环境中&#xff0c;企业最大的危机往往不是来自外部&#xff0c;而是内部的僵化与活力丧失。如何打破大企业病&#xff1f;如何让听得见炮声的人呼唤炮火&#xff1f;本文结合华为资深专家吕远洋的分享&#xff0c;为你揭秘激活组织活力的BRAVE模型与管理增效的…

作者头像 李华
网站建设 2026/5/15 7:22:49

娴嬭瘯鏂囩珷

娴嬭瘯鏂囩珷 杩欐槸涓&#xfffd;涓祴璇曟枃绔犵殑鍐呭锛岀敤浜庨獙璇丆SDN鏂囩珷鍙戝竷鍔熻兘鏄惁姝&#xff45;父宸ヤ綔銆&#xfffd;

作者头像 李华
网站建设 2026/5/15 3:13:29

pytest 在命令行调试单个测试用例

在进行 Python 测试时&#xff0c;我们经常需要针对性地运行或调试单个测试用例&#xff0c;而不是执行整个测试套件。pytest 提供了多种灵活的方式来实现这一需求。本文将详细介绍如何在命令行中精准地调试单个测试用例。 环境准备 创建示例测试文件 test_math_operations.py&…

作者头像 李华
网站建设 2026/5/20 22:49:34

谁懂啊!这些专业论文 AI 写作软件,拯救我的毕业论文

作为一名应届毕业生&#xff0c;最近的生活被毕业论文按在地上反复摩擦&#xff0c;谁懂这种焦虑啊&#xff01;熬了好几个大夜&#xff0c;选题改了八遍&#xff0c;框架被导师打回五次&#xff0c;好不容易憋出初稿&#xff0c;查重率直接飙到 40%&#xff0c;对着满屏的红色…

作者头像 李华
网站建设 2026/5/23 0:43:58

mirror_fold.py_utils_0207curso

import osimport randomimport timefrom typing import Dict, Optional, Tupleimport numpy as np# 后视镜折叠场景配置&#xff08;请按你的4种分辨率填写&#xff09;# key: (width, height) value: (x1, y1, x2, y2) 车辆黑色区域在原图上的像素坐标MIRROR_FOLD_CAR_BOXES:…

作者头像 李华
网站建设 2026/5/22 15:14:27

2026年博士论文去AIGC痕迹:10%以下达标攻略

2026年博士论文去AIGC痕迹&#xff1a;10%以下达标攻略 博士论文AI率要求最严格&#xff1a;10%以下&#xff0c;部分985高校甚至要求5%以下。 我一个博士师兄&#xff0c;论文AI率12%&#xff0c;本来以为稳了&#xff0c;结果学校要求10%以下&#xff0c;只差2个点被打回来…

作者头像 李华