You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI中创建日期对齐、天数准确的动态日历网格方案问询

动态SwiftUI日历:解决日期对齐、天数计算及空白单元格问题

问题描述

我正尝试在SwiftUI中开发一款动态日历,要求满足以下功能:

  • 每个月份显示正确天数(如2月28/29天、4月30天)
  • 日期与对应星期正确对齐(如12月1日对应周四)
  • 无效日期(如4月31日)显示为空单元格
  • 在月初前、月末后补充空白单元格

目前已编写CalendarCell代码,但无法实现上述动态生成天数、日期对齐及无效日期空单元格的处理,想知道遗漏了什么,以及如何修改代码实现需求。

现有代码

struct CalendarCell: View {
    let group: GroupT
    @EnvironmentObject var dateHolder: DateHolder
    @StateObject var eventViewModel: EventViewModel
    let count: Int
    let startingSpaces: Int
    let daysInMonth: Int
    let daysInPrevMonth: Int
    @State private var events: [Event] = []
    @State private var isShowingDailyEventPage = false
    @State private var isLoading = false

    var body: some View {
        let textColor = self.textColor(type: self.monthStruct().monthType)
        
        VStack {
            // Show day only for current month
            if monthStruct().monthType == .Current {
                Text(monthStruct().day())
                    .font(.system(size: 16, weight: .semibold))
                    .foregroundColor(textColor)
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .background(isLoading ? Color.gray.opacity(0.2) : Color.clear)
                    .cornerRadius(8)
                    .overlay(
                        Group {
                            if textColor == .black {
                                Button(action: {
                                    fetchDailyEvents()
                                    isShowingDailyEventPage = true
                                }) {
                                    Image(systemName: "questionmark.circle")
                                        .foregroundColor(Color.gray)
                                        .imageScale(.small)
                                        .padding(4)
                                }
                                .sheet(isPresented: $isShowingDailyEventPage) {
                                    DailyEventPage(date: dateForCell(), group: group, events: events, eventViewModel: eventViewModel)
                                        .onAppear {
                                            fetchDailyEvents()
                                        }
                                }
                            }
                        },
                        alignment: .topTrailing
                    )
                
                // Event indicator if events are present
                if !events.isEmpty {
                    Circle()
                        .fill(Color.blue)
                        .frame(width: 6, height: 6)
                        .padding(.top, 2)
                }
            }

            // Show loading indicator
            if isLoading {
                ProgressView("Loading Events...")
                    .progressViewStyle(CircularProgressViewStyle(tint: .blue))
                    .padding()
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
            }
        }
    }

    // Function to fetch events
    func fetchDailyEvents() {
        isLoading = true
        let carpoolID = group.carpoolID
        let joinCode = group.joinCode

        eventViewModel.fetchDailyEvents(forCarpoolID: carpoolID.uuidString, date: dateForCell()) { result in
            switch result {
            case .success(let events):
                DispatchQueue.main.async {
                    self.events = events
                    self.isLoading = false
                }
            case .failure(let error):
                DispatchQueue.main.async {
                    print("Error fetching events: \(error)")
                    self.isLoading = false
                }
            }
        }
    }

    // Helper functions for determining month type and day
    func monthStruct() -> MonthStruct {
        let start = startingSpaces == 0 ? startingSpaces + 7 : startingSpaces
        if count <= start {
            let day = daysInPrevMonth + count - start
            return MonthStruct(monthType: MonthType.Previous, dayInt: day)
        } else if count - start > daysInMonth {
            let day = count - start - daysInMonth
            return MonthStruct(monthType: MonthType.Next, dayInt: day)
        }
        let day = count - start
        return MonthStruct(monthType: MonthType.Current, dayInt: day)
    }

    // Convert day to Date
    private func dateForCell() -> Date {
        let firstOfMonth = Calendar.current.date(from: Calendar.current.dateComponents([.year, .month], from: dateHolder.date))!
        return Calendar.current.date(byAdding: .day, value: monthStruct().dayInt - 1, to: firstOfMonth)!
    }
}

问题分析与修改方案

1. 核心遗漏点

  • 非当前月份日期未处理:仅显示当月日期,导致月初/月末的空白单元格无占位,网格对齐混乱
  • 起始偏移逻辑错误:monthStruct()中对start的额外计算会破坏星期对齐
  • 无效日期无判断:未验证日期是否存在于对应月份(如4月31日)
  • 单元格布局不稳定:非当月日期完全隐藏,导致单元格高度/宽度不一致

2. 具体修改步骤

步骤1:修复日期偏移计算逻辑

删除错误的start调整,直接使用传入的startingSpaces(该值需在父视图中提前计算为当月1号的星期偏移量):

func monthStruct() -> MonthStruct {
    if count <= startingSpaces {
        // 上月日期
        let day = daysInPrevMonth + count - startingSpaces
        return MonthStruct(monthType: MonthType.Previous, dayInt: day)
    } else if count - startingSpaces > daysInMonth {
        // 下月日期
        let day = count - startingSpaces - daysInMonth
        return MonthStruct(monthType: MonthType.Next, dayInt: day)
    }
    // 当月日期
    let day = count - startingSpaces
    return MonthStruct(monthType: MonthType.Current, dayInt: day)
}

步骤2:完善单元格布局,显示所有月份日期

修改body,让非当月日期也显示(置灰),确保所有单元格保留占位:

var body: some View {
    let monthInfo = monthStruct()
    let textColor = self.textColor(type: monthInfo.monthType)
    
    VStack {
        // 所有月份类型的日期都显示,无效日期为空文本
        Text(monthInfo.monthType == .Invalid ? "" : monthInfo.day())
            .font(.system(size: 16, weight: .semibold))
            .foregroundColor(textColor)
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(isLoading ? Color.gray.opacity(0.2) : Color.clear)
            .cornerRadius(8)
            // 仅当月日期添加事件按钮
            .overlay(
                Group {
                    if monthInfo.monthType == .Current {
                        Button(action: {
                            fetchDailyEvents()
                            isShowingDailyEventPage = true
                        }) {
                            Image(systemName: "questionmark.circle")
                                .foregroundColor(Color.gray)
                                .imageScale(.small)
                                .padding(4)
                        }
                        .sheet(isPresented: $isShowingDailyEventPage) {
                            DailyEventPage(date: dateForCell(), group: group, events: events, eventViewModel: eventViewModel)
                                .onAppear {
                                    fetchDailyEvents()
                                }
                        }
                    }
                },
                alignment: .topTrailing
            )
        
        // 仅当月日期显示事件指示器
        if monthInfo.monthType == .Current && !events.isEmpty {
            Circle()
                .fill(Color.blue)
                .frame(width: 6, height: 6)
                .padding(.top, 2)
        }

        // 加载指示器
        if isLoading {
            ProgressView("Loading Events...")
                .progressViewStyle(CircularProgressViewStyle(tint: .blue))
                .padding()
                .frame(maxWidth: .infinity, maxHeight: .infinity)
        }
    }
    // 强制单元格占据固定空间,避免布局错乱
    .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
}

步骤3:添加无效日期判断

首先扩展MonthType枚举,新增.Invalid类型:

enum MonthType {
    case Current, Previous, Next, Invalid
}

然后添加日期有效性验证方法:

private func isDayValid(day: Int, monthType: MonthType) -> Bool {
    guard let currentDate = dateHolder.date else { return false }
    let calendar = Calendar.current
    
    switch monthType {
    case .Current:
        let range = calendar.range(of: .day, in: .month, for: currentDate)!
        return range.contains(day)
    case .Previous:
        guard let prevMonthDate = calendar.date(byAdding: .month, value: -1, to: currentDate) else { return false }
        let range = calendar.range(of: .day, in: .month, for: prevMonthDate)!
        return range.contains(day)
    case .Next:
        guard let nextMonthDate = calendar.date(byAdding: .month, value: 1, to: currentDate) else { return false }
        let range = calendar.range(of: .day, in: .month, for: nextMonthDate)!
        return range.contains(day)
    default:
        return false
    }
}

最后修改monthStruct(),返回无效日期类型:

func monthStruct() -> MonthStruct {
    if count <= startingSpaces {
        let day = daysInPrevMonth + count - startingSpaces
        return isDayValid(day: day, monthType: .Previous) 
            ? MonthStruct(monthType: .Previous, dayInt: day) 
            : MonthStruct(monthType: .Invalid, dayInt: 0)
    } else if count - startingSpaces > daysInMonth {
        let day = count - startingSpaces - daysInMonth
        return isDayValid(day: day, monthType: .Next) 
            ? MonthStruct(monthType: .Next, dayInt: day) 
            : MonthStruct(monthType: .Invalid, dayInt: 0)
    }
    let day = count - startingSpaces
    return isDayValid(day: day, monthType: .Current) 
        ? MonthStruct(monthType: .Current, dayInt: day) 
        : MonthStruct(monthType: .Invalid, dayInt: 0)
}

步骤4:确保父视图参数计算正确

在父视图中准确计算startingSpaces、daysInMonth、daysInPrevMonth:

func calculateMonthParameters(for date: Date) -> (startingSpaces: Int, daysInMonth: Int, daysInPrevMonth: Int) {
    let calendar = Calendar.current
    let components = calendar.dateComponents([.year, .month], from: date)
    guard let firstOfMonth = calendar.date(from: components) else { return (0,0,0) }
    
    // 计算当月1号的星期偏移量(适配系统首周设置)
    let weekday = calendar.component(.weekday, from: firstOfMonth)
    let startingSpaces = weekday - calendar.firstWeekday
    
    // 当月天数
    let daysInMonth = calendar.range(of: .day, in: .month, for: firstOfMonth)?.count ?? 0
    
    // 上月天数
    guard let prevMonth = calendar.date(byAdding: .month, value: -1, to: firstOfMonth) else { return (0,0,0) }
    let daysInPrevMonth = calendar.range(of: .day, in: .month, for: prevMonth)?.count ?? 0
    
    return (startingSpaces, daysInMonth, daysInPrevMonth)
}

3. 额外优化建议

  • 缓存monthStruct()的计算结果,避免View渲染时重复计算
  • 为非当月日期添加透明度(如.opacity(0.5)),增强视觉区分
  • 确保MonthStruct的day()方法能正确将dayInt转换为字符串

内容的提问来源于stack exchange,提问作者Nick

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 16:13:11