Calendar — 什么是Date日历和Swift中的计算

作者: IT Sectr 发布日期: 2026-07-12 阅读时间: 7 分钟

Calendar — 是一个Foundation类,它定义了日历系统并提供了日历计算的方法:提取日期组件、计算日期之间的差异、查找周期边界和移动日期。日历将绝对时间(Date)与人可读的组件关联起来,并考虑区域特性:周的开始、时区和夏令时。根据Apple Developer Documentation(2025),Foundation支持17种日历系统——从公历到佛教和日本历,这使得Calendar成为国际化应用程序的通用工具。

要点

  • Calendar — 用于日历计算的Foundation类:提取组件、比较和移动日期。
  • Calendar.current — 用户的系统日历,自动考虑区域设置。
  • 17种日历系统 — Foundation支持公历、佛教、日本、希伯来、伊斯兰等。
  • Calendar.dateComponents 从Date中提取组件(年、月、日),同时考虑时区。
  • Calendar.dateInterval 返回指定周期(日、周、月)的开始和结束日期。

Foundation中的Calendar是什么?

Calendar — 是一个Foundation类,基于ICU(International Components for Unicode)实现日历计算。日历确定绝对时间(Date)如何映射到日历组件:年、月、日、时、分、秒。没有Calendar,就无法知道今天是哪一年、哪一月、哪一日——Date本身不包含这些信息。

日历考虑三组参数:日历系统(公历、佛教、日本)、时区区域设置。Calendar.current从用户的系统设置中组合所有三个。Calendar.autoupdatingCurrent — 一个特殊版本,当设置更改时通过NotificationCenter自动更新,无需重新启动应用程序。

日历是Foundation中的值类型(value type)。Calendar(identifier:)使用固定参数创建一个新实例。日历可以复制、使用==比较并用作字典中的键。这允许创建具有特定timeZone和locale设置的日历用于测试。

Calendar和NSCalendar

Calendar — Objective-C NSCalendar的Swift版本,使用as Calendar / as NSCalendar桥接。在现代Swift中,到处都使用Calendar。NSCalendar保留用于与Objective-C API向后兼容。Calendar拥有完整的方法集,没有NS前缀,具有类型安全参数和Swift可选性。

线程安全 — Calendar对于读取是线程安全的。创建的实例可以从多个线程安全地读取。修改属性(timeZone、locale)不是线程安全的——对于不同的配置,请创建单独的Calendar实例。

Foundation中的日历类型

Foundation通过Calendar.Identifier枚举支持17种日历系统。每个系统都有自己的闰年规则、月份数量和纪元开始时间。日历的选择影响所有计算:dateComponents、dateInterval、nextDate。

主要日历系统:

  • .gregorian — 国际标准,12个月,365/366天,公元纪元。
  • .buddhist — 佛教日历,在泰国、柬埔寨、老挝使用,纪元比公历早543年。
  • .japanese — 日本日历基于天皇统治纪元,12个月如公历。
  • .hebrew — 希伯来历,12/13个月,闰月Adar II。
  • .islamic — 伊斯兰历,12个太阴月,354/355天。
  • .indian — 印度国历(Saka),12个月。

Calendar(identifier: .gregorian) — 最常用的。符合国际标准ISO 8601,是大多数国家的默认日历。对于面向国际用户的应用程序,使用Calendar.current — 它自动匹配用户的系统日历。

标识符类型使用区域
.gregorian太阳国际
.buddhist太阳泰国、柬埔寨
.japanese太阳日本
.hebrew太阴-太阳以色列
.islamic太阴伊斯兰国家
.chinese太阴-太阳中国

Calendar和DateComponents

DateComponentsCalendar — 不可分离的一对。Calendar.dateComponents(_:from:)从Date中提取组件,同时考虑日历的时区。Calendar.date(from:)从DateComponents构建Date,用默认值填充缺失字段:日=1,时=0,分=0,秒=0。

Calendar.component方法提取一个组件,方便快速检查。Calendar.dateComponents在一次调用中提取一组组件——这更高效,因为Calendar执行一次日历计算,而不是为每个组件单独执行。对于3个以上组件的列表,始终使用dateComponents。

Calendar.compare以指定的精度比较两个Date。参数toGranularity确定比较执行到哪个组件:.year只比较年份,.month — 年份和月份,.day — 年、月、日。这方便检查两个日期是否属于同一天,而无需考虑时间。

swift
let calendar = Calendar.current
let now = Date()

// 提取单个组件
let year = calendar.component(.year, from: now)

// 提取一组组件
let comps = calendar.dateComponents(
    [.year, .month, .day], from: now
)

// 按天精度比较
let isSameDay = calendar.compare(date1, to: date2,
                                  toGranularity: .day) == .orderedSame

// 检查日期是否为今天
let isToday = calendar.isDateInToday(someDate)

Calendar.isDateInTodayisDateInTomorrowisDateInYesterday — 用于相对检查的方法。Calendar.isDate(_:inSameDayAs:)检查两个日期是否落在同一个日历日,同时考虑日历的时区。这些方法内部使用Calendar.compare,并针对频繁调用进行了优化。

Calendar计算

Calendar.dateInterval — 对分析和UI最有用的方法之一。它返回指定组件的DateInterval:日、周、月、年的开始和结束。DateInterval包含start(Date)和end(Date)——周期的边界。例如,dateInterval(of: .weekOfYear, for: Date())返回当前周星期一的开始和星期日的结束。

Calendar.date与byAdding — 移动日期的方法。Calendar.date(byAdding: .day, value: 7, to: Date())返回一周后的日期。Calendar.date(byAdding: DateComponents) — 更灵活的版本,允许同时移动多个组件:+1个月+3天。Calendar自动考虑不同月份的长度和闰年。

Calendar.nextDate搜索与指定DateComponents匹配的下一个日期。参数matchingPolicy确定不匹配时的行为:.nextTime — 下一次时间匹配,.nextTimePreservingSmallerComponents — 保留原始日期的分钟和秒,.strict — 需要精确匹配。

swift
let calendar = Calendar.current
let today = Date()

// 一周的开始和结束
let weekInterval = calendar.dateInterval(
    of: .weekOfYear, for: today
)!

// 移动1个月
let nextMonth = calendar.date(
    byAdding: .month, value: 1, to: today
)!

// 通过DateComponents移动
var delta = DateComponents()
delta.month = 1
delta.day = 3
let shifted = calendar.date(byAdding: delta, to: today)!

// 下一个13号星期五
let friday13Components = DateComponents(
    weekday: 6, day: 13
)
let nextFriday13 = calendar.nextDate(
    after: today, matching: friday13Components,
    matchingPolicy: .nextTime
)

EnumerateDates — 一个强大的方法,用于按模式迭代日期。Calendar.enumerateDates(startingAfter:matching:matchingPolicy:using:)为每个匹配调用一个块,直到块返回stop=true。用于生成日历和日程中的重复事件。该方法比使用nextDate的手动循环更高效,因为它由ICU优化。

TimeZone和Locale

TimeZone — Calendar不可分割的一部分。时区确定绝对Date对应的日历时间。相同的Date在UTC和莫斯科给出不同的组件:Date()在UTC可能显示10:00,而在MSK显示13:00。Calendar.timeZone默认等于TimeZone.current。

Locale影响一周的第一天、一年中第一周的最小天数(minDaysInFirstWeek)以及月份/星期几的名称(通过DateFormatter转换时)。Calendar.locale默认等于Locale.current。在中文区域设置中,一周从星期一开始,在美国区域设置中——从星期日开始。

Calendar.availableIdentifiers返回所有支持的日历标识符列表。static property Calendar.availableCalendarIdentifiers — 具有相同标识符的字符串数组。用于构建日历选择UI以及检查设备上特定日历系统的可用性。

swift
// 具有特定时区的日历
var utcCalendar = Calendar(identifier: .gregorian)
utcCalendar.timeZone = TimeZone(identifier: "UTC")!

// 具有俄语区域设置的日历
var russianCalendar = Calendar(identifier: .gregorian)
russianCalendar.locale = Locale(identifier: "ru_RU")

// 一周的第一天取决于区域设置
let firstWeekday = russianCalendar.firstWeekday
// 2 = 星期一(在zh_CN中)

// 可用日历列表
for identifier in Calendar.availableIdentifiers {
    print(identifier)
}

firstWeekday — Calendar的属性,确定一周中的哪一天被视为第一天。在中文区域设置中,Sunday=2(星期一第一天)。在美国区域设置中,Sunday=1。这影响weekOfMonth和weekOfYear的工作方式:相同的日期在不同区域设置中可能被分配到不同的周数。对于处理日期的应用程序,使用Calendar.current或显式设置firstWeekday。

使用Calendar的示例

让我们看看展示Calendar功能的实际场景。每个示例解决一个特定的iOS开发任务,并展示日历计算的正确使用方法。

检查:日期在本月吗?

Calendar.dateInterval(of: .month, for:)返回当前月份的边界。检查Date是否在此区间内——判断日期是否属于当前月份的最快方法。替代方法 — Calendar.compare使用granularity .month:如果结果为.orderedSame,则月份匹配。

swift
func isInCurrentMonth(_ date: Date) -> Bool {
    let calendar = Calendar.current
    let monthInterval = calendar.dateInterval(
        of: .month, for: Date()
    )!
    return monthInterval.contains(date)
}

// 一个月中的天数
func daysInMonth(for date: Date) -> Int {
    let calendar = Calendar.current
    return calendar.range(
        of: .day, in: .month, for: date
    )?.count ?? 0
}

// 正确结束的月份添加
func addMonths(_ months: Int, to date: Date) -> Date {
    let calendar = Calendar.current
    return calendar.date(
        byAdding: .month, value: months, to: date
    )!
}

Calendar.range(of:in:for:)返回指定组件在另一个组件上下文中的允许值范围。例如,range(of: .day, in: .month, for: date)对于31天的月份返回1..<32,对于非闰年的二月返回1..<29。这是了解一个月中天数的正确方法,而不是使用硬编码值。

添加月份通过Calendar.date(byAdding:value:to:)正确处理边界日期。如果1月31日加1个月,Calendar返回2月28日(或闰年的2月29日),而不是3月3日,就像通过TimeInterval简单加30天那样。这是不使用TimeInterval进行日历计算的另一个原因。

Calendar方法用途示例
dateInterval周期边界月的开始和结束
range(of:in:for:)组件范围当前月份的天数
date(byAdding:)移动日期从今天起+1个月
isDateInToday检查是否为今天日期是否是今天
compare(toGranularity:)精确比较同一天不考虑时间

常见问题

Calendar.current和Calendar(identifier: .gregorian)之间有什么区别?

Calendar.current从用户的系统设置中返回日历——可能不是公历(例如,泰国的佛教历)。Calendar(identifier: .gregorian)始终创建公历,无论设置如何。显示日期时使用Calendar.current,业务逻辑中使用显式选择的标识符。

为什么Calendar.date(byAdding: .month, value: 1)有时返回相同的日期?

这与不同月份的长度有关。如果当前日期是1月31日,加1个月得到2月28日,因为2月没有31天。Calendar自动将日期补全到月份的最后允许日期。要精确控制,使用DateComponents与day:1来转到月份的第一天。

DateFormatter默认使用哪个日历?

DateFormatter使用Calendar.current — 用户的系统日历。如果应用程序无论设置如何都应始终在公历中显示日期,请设置formatter.calendar = Calendar(identifier: .gregorian)。这保证了所有用户的一致显示。

如何通过Calendar检查一年是否是闰年?

Calendar.range(of: .day, in: .year, for: date)返回365或366天。更简单:Calendar.date(from: DateComponents(year: 年, month: 2, day: 29)) != nil ——如果2月29日存在,则该年是闰年。Calendar本身考虑特定日历系统的规则。

创建Calendar后能否更改firstWeekday?

可以,firstWeekday属性是可写的。更改影响weekOfMonth、weekOfYear以及所有与周数相关的计算。当设置locale = Locale(identifier: "zh_CN")时,firstWeekday自动变为2(星期一)。手动设置会覆盖来自locale的值。

总结

  • Calendar — 用于日历计算的Foundation类,通过DateComponents将Date与人可读的组件关联起来。
  • 17种日历系统由Foundation支持——从公历到佛教和日本历,自动考虑区域规则。
  • Calendar.current匹配用户的系统日历——在UI和DateFormatter中使用它。
  • Calendar.dateIntervalCalendar.range — 处理周期边界和组件范围的关键方法。
  • Calendar.date(byAdding:)正确处理不同月份的长度和闰年——不要用TimeInterval算术替代它。
  • TimeZone和Locale作为Calendar的属性影响所有日历计算——显式设置它们以实现可预测的行为。
  • firstWeekday由区域设置确定并影响周数——对于全局应用程序,显式设置它或使用Calendar.current。

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读