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

SwiftUI无第三方包日历开发:日期与星期错位问题求助

SwiftUI日历日期错位问题修复

你的核心问题在于:直接从1号开始填充日历格子,没有考虑当月第一天对应的星期位置,导致1号固定落在第一个格子(周日),完全忽略了实际月份的起始星期。

修复思路

  1. 计算当月第一天是星期几,得出需要在1号前预留的空格子数量
  2. 在日历网格开头添加对应数量的空占位符
  3. 可选:在月末添加空占位符,让日历保持固定的6行高度(提升视觉一致性)

修改后的完整代码

struct CustomCalendarView: View {
    let calendar = Calendar.current
    let daysOfWeek = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
    @State private var selectedDate: Date = Date()

    var body: some View {
        VStack {
            HStack {
                Button(action: {
                    navigateToPreviousMonth()
                }) {
                    Image(systemName: "chevron.left")
                        .font(.headline)
                }
                .padding(.trailing, 8)

                Text(monthYearFormatter.string(from: selectedDate))
                    .font(.title)
                    .padding(.bottom, 10)

                Button(action: {
                    navigateToNextMonth()
                }) {
                    Image(systemName: "chevron.right")
                        .font(.headline)
                }
                .padding(.leading, 8)
            }

            HStack {
                ForEach(daysOfWeek, id: \.self) { day in
                    Text(day)
                        .font(.headline)
                        .frame(maxWidth: .infinity)
                }
            }

            LazyVGrid(columns: Array(repeating: .init(.flexible()), count: 7)) {
                // 1. 添加前置空占位符,对齐星期位置
                ForEach(0..<leadingEmptyDays(), id: \.self) { _ in
                    Text("")
                        .frame(maxWidth: .infinity)
                        .padding(8)
                }
                
                // 2. 当月日期(原逻辑保留)
                ForEach(1...daysInMonth(date: selectedDate), id: \.self) { day in
                    Text("\(day)")
                        .font(.headline)
                        .frame(maxWidth: .infinity)
                        .padding(8)
                        .background(dayBackgroundColor(for: day))
                        .foregroundColor(dayForegroundColor(for: day))
                        .clipShape(Circle())
                        .onTapGesture {
                            selectDate(day: day)
                        }
                }
                
                // 3. 可选:添加后置空占位符,保持日历6行高度
                let totalCells = leadingEmptyDays() + daysInMonth(date: selectedDate)
                let trailingEmptyDays = (6 * 7) - totalCells
                if trailingEmptyDays > 0 {
                    ForEach(0..<trailingEmptyDays, id: \.self) { _ in
                        Text("")
                            .frame(maxWidth: .infinity)
                            .padding(8)
                    }
                }
            }
        }
        .padding()
    }

    // 新增:获取当月第一天
    private func firstDayOfMonth(date: Date) -> Date {
        let components = calendar.dateComponents([.year, .month], from: date)
        return calendar.date(from: components)!
    }

    // 新增:计算需要前置的空格子数量
    private func leadingEmptyDays() -> Int {
        let firstDay = firstDayOfMonth(date: selectedDate)
        // Calendar.current的weekday从1(周日)开始,所以减1得到空位数
        return calendar.component(.weekday, from: firstDay) - 1
    }

    // 以下为原代码,无需修改
    private func selectDate(day: Int) {
        var components = calendar.dateComponents([.year, .month, .day], from: selectedDate)
        components.day = day
        guard let updatedDate = calendar.date(from: components) else {
            return
        }
        selectedDate = updatedDate
    }

    private func daysInMonth(date: Date) -> Int {
        guard let range = calendar.range(of: .day, in: .month, for: date) else {
            return 0
        }
        return range.count
    }

    private func dayBackgroundColor(for day: Int) -> Color {
        let isCurrentMonth = calendar.isDate(selectedDate, equalTo: Date(), toGranularity: .month)
        let isSelected = calendar.isDate(selectedDate, equalTo: date(withDay: day), toGranularity: .day)
        return isCurrentMonth && isSelected ? .blue : .clear
    }

    private func dayForegroundColor(for day: Int) -> Color {
        let isCurrentMonth = calendar.isDate(selectedDate, equalTo: Date(), toGranularity: .month)
        let isSelected = calendar.isDate(selectedDate, equalTo: date(withDay: day), toGranularity: .day)
        return isCurrentMonth && isSelected ? .white : .primary
    }

    private func date(withDay day: Int) -> Date {
        var components = calendar.dateComponents([.year, .month, .day], from: selectedDate)
        components.day = day
        return calendar.date(from: components) ?? Date()
    }

    private var monthYearFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "MMMM yyyy"
        return formatter
    }()

    private func navigateToPreviousMonth() {
        guard let previousMonth = calendar.date(byAdding: .month, value: -1, to: selectedDate) else {
            return
        }
        selectedDate = previousMonth
    }

    private func navigateToNextMonth() {
        guard let nextMonth = calendar.date(byAdding: .month, value: 1, to: selectedDate) else {
            return
        }
        selectedDate = nextMonth
    }
}

关键逻辑说明

  • firstDayOfMonth:提取当前选中月份的第一天日期
  • leadingEmptyDays:通过calendar.component(.weekday, from: firstDay)获取第一天的星期值(1=周日,2=周一…7=周六),减1后就是需要前置的空格子数量,确保1号能落在正确的星期列下
  • 后置空占位符是可选优化,让所有月份的日历都保持6行高度,避免切换月份时布局跳动

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.18 11:44:56