Calendar — 是一个Foundation类,它定义了日历系统并提供了日历计算的方法:提取日期组件、计算日期之间的差异、查找周期边界和移动日期。日历将绝对时间(Date)与人可读的组件关联起来,并考虑区域特性:周的开始、时区和夏令时。根据Apple Developer Documentation(2025),Foundation支持17种日历系统——从公历到佛教和日本历,这使得Calendar成为国际化应用程序的通用工具。
要点
Calendar — 是一个Foundation类,基于ICU(International Components for Unicode)实现日历计算。日历确定绝对时间(Date)如何映射到日历组件:年、月、日、时、分、秒。没有Calendar,就无法知道今天是哪一年、哪一月、哪一日——Date本身不包含这些信息。
日历考虑三组参数:日历系统(公历、佛教、日本)、时区和区域设置。Calendar.current从用户的系统设置中组合所有三个。Calendar.autoupdatingCurrent — 一个特殊版本,当设置更改时通过NotificationCenter自动更新,无需重新启动应用程序。
日历是Foundation中的值类型(value type)。Calendar(identifier:)使用固定参数创建一个新实例。日历可以复制、使用==比较并用作字典中的键。这允许创建具有特定timeZone和locale设置的日历用于测试。
Calendar — Objective-C NSCalendar的Swift版本,使用as Calendar / as NSCalendar桥接。在现代Swift中,到处都使用Calendar。NSCalendar保留用于与Objective-C API向后兼容。Calendar拥有完整的方法集,没有NS前缀,具有类型安全参数和Swift可选性。
线程安全 — Calendar对于读取是线程安全的。创建的实例可以从多个线程安全地读取。修改属性(timeZone、locale)不是线程安全的——对于不同的配置,请创建单独的Calendar实例。
Foundation通过Calendar.Identifier枚举支持17种日历系统。每个系统都有自己的闰年规则、月份数量和纪元开始时间。日历的选择影响所有计算:dateComponents、dateInterval、nextDate。
主要日历系统:
Calendar(identifier: .gregorian) — 最常用的。符合国际标准ISO 8601,是大多数国家的默认日历。对于面向国际用户的应用程序,使用Calendar.current — 它自动匹配用户的系统日历。
| 标识符 | 类型 | 使用区域 |
|---|---|---|
| .gregorian | 太阳 | 国际 |
| .buddhist | 太阳 | 泰国、柬埔寨 |
| .japanese | 太阳 | 日本 |
| .hebrew | 太阴-太阳 | 以色列 |
| .islamic | 太阴 | 伊斯兰国家 |
| .chinese | 太阴-太阳 | 中国 |
DateComponents和Calendar — 不可分离的一对。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 — 年、月、日。这方便检查两个日期是否属于同一天,而无需考虑时间。
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.isDateInToday、isDateInTomorrow、isDateInYesterday — 用于相对检查的方法。Calendar.isDate(_:inSameDayAs:)检查两个日期是否落在同一个日历日,同时考虑日历的时区。这些方法内部使用Calendar.compare,并针对频繁调用进行了优化。
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 — 需要精确匹配。
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 — 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以及检查设备上特定日历系统的可用性。
// 具有特定时区的日历
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功能的实际场景。每个示例解决一个特定的iOS开发任务,并展示日历计算的正确使用方法。
Calendar.dateInterval(of: .month, for:)返回当前月份的边界。检查Date是否在此区间内——判断日期是否属于当前月份的最快方法。替代方法 — Calendar.compare使用granularity .month:如果结果为.orderedSame,则月份匹配。
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,业务逻辑中使用显式选择的标识符。
这与不同月份的长度有关。如果当前日期是1月31日,加1个月得到2月28日,因为2月没有31天。Calendar自动将日期补全到月份的最后允许日期。要精确控制,使用DateComponents与day:1来转到月份的第一天。
DateFormatter使用Calendar.current — 用户的系统日历。如果应用程序无论设置如何都应始终在公历中显示日期,请设置formatter.calendar = Calendar(identifier: .gregorian)。这保证了所有用户的一致显示。
Calendar.range(of: .day, in: .year, for: date)返回365或366天。更简单:Calendar.date(from: DateComponents(year: 年, month: 2, day: 29)) != nil ——如果2月29日存在,则该年是闰年。Calendar本身考虑特定日历系统的规则。
可以,firstWeekday属性是可写的。更改影响weekOfMonth、weekOfYear以及所有与周数相关的计算。当设置locale = Locale(identifier: "zh_CN")时,firstWeekday自动变为2(星期一)。手动设置会覆盖来自locale的值。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。