DateComponents 是一个 Foundation 结构体,它将日历日期的各个组件存储为单独的字段:年、月、日、小时、分钟、秒等。与表示绝对时间点的 Date 不同,DateComponents 包含人类可读的值,这些值依赖于日历和时区。根据 Apple Developer Documentation (2025),DateComponents 被用作 Date 和 Calendar 之间的中间环节——通过它来提取和构造日历日期,无需手动运算即可执行日期计算和偏移。
要点
DateComponents 是一个 Foundation 值类型,用于存储时间的日历组件。每个组件表示为一个可选的 Int 字段:year、month、day、hour、minute、second、nanosecond、weekday、weekOfMonth、weekOfYear、quarter、yearForWeekOfYear 等。
与 Date 的主要区别在于与日历的关联。Date 存储绝对时间(从参考日期开始的秒数),而 DateComponents 是一种人类可读的表示形式,仅在特定 Calendar 的上下文中有意义。同一个 Date 可以在不同的日历和时区中以不同的 DateComponents 表示。
DateComponents 不是一个独立的时间类型,而是一个数据容器。要将 DateComponents 解释为日期,需要 Calendar 来理解组件如何与日历系统关联。Calendar.dateComponents(from: Date) 执行组件提取,Calendar.date(from: DateComponents) 执行反向组装。
DateComponents 的每个字段都是可选的(Int?),这对于处理不完整的日期至关重要。如果只指定年和月,Calendar 将使用默认值填充缺失的字段:日 = 1,小时 = 0,分钟 = 0。这对于创建期间开始日期很方便——只需指定感兴趣的组件即可。
使用 == 运算符比较 DateComponents 时,只比较已指定(非 nil)的字段。两个具有 2026 年但月份不同的 DateComponents 结构体被认为是不同的。来自 NSObjectProtocol 的 isEqual 不适用于 DateComponents——DateComponents 不继承自 NSObject。
基本字段 DateComponents 包括 year、month、day、hour、minute、second、nanosecond。每个字段以相应的单位存储数值:年 – 2026,月 – 1..12,日 – 1..31,小时 – 0..23,分钟 – 0..59,秒 – 0..59。纳秒可以取值 0..999999999。
周字段 – weekday(1..7,其中 1 = 公历中的星期日)、weekOfMonth、weekOfYear。这些字段依赖于 Calendar,在其上下文之外没有意义。weekday 取决于日历的 firstWeekday 设置:在中国区域设置中,一周从星期一开始(公历中 weekday = 2),而在美国区域设置中——从星期日开始(weekday = 1)。
专用字段 – quarter(1..4)、yearForWeekOfYear(周所属的年份)、isLeapMonth(希伯来或中国日历中闰月的逻辑标志)。calendar 和 timeZone 字段存储对创建结构时所使用的相应对象的引用。
| 类别 | 字段 | 范围 |
|---|---|---|
| 日历 | year, month, day | 1..∞, 1..12, 1..31 |
| 时间 | hour, minute, second, nanosecond | 0..23, 0..59, 0..59, 0..999999999 |
| 周 | weekday, weekOfMonth, weekOfYear | 1..7, 1..5, 1..53 |
| 特殊 | quarter, yearForWeekOfYear | 1..4, 依赖 |
通过 Calendar.dateComponents 提取组件时,为了性能只请求必要的字段很重要。Calendar 在单次遍历中提取所有请求的字段——这比单独为每个字段调用 Calendar.component 要快得多。
DateComponents 的初始化——最简单的方法:创建一个空结构体并填充必要的字段。所有未指定的字段自动获得 nil。由部分组件创建的日期在初始化阶段不会进行验证——只有在通过 Calendar 转换为 Date 时才可能出现错误。
DateComponents(calendar:timeZone:era:year:month:day:hour:minute:second:nanosecond:weekday:…) 初始化器允许在一次调用中设置所有字段。这个初始化器便于从现成的值创建完整日期,但由于可读性,很少用于超过 5-6 个参数。
Calendar.dateComponents(_:from:)——从现有 Date 获取 DateComponents 的主要方式。第二个参数是要提取的组件集合。Calendar 在考虑时区的情况下执行日历计算,并返回仅包含请求字段的结构体,其余字段保持为 nil。
import Foundation
// 通过字段初始化器创建
var components = DateComponents()
components.year = 2026
components.month = 7
components.day = 21
// 从 Date 提取
let now = Date()
let extracted = Calendar.current.dateComponents(
[.year, .month, .day],
from: now
)
print("Today: \(extracted.day!).\(extracted.month!).\(extracted.year!)")
// 通过扩展初始化器创建
let birthday = DateComponents(
calendar: Calendar.current,
year: 1990, month: 5, day: 15
)
通过手动字段创建 DateComponents 时,在转换为 Date 之前始终检查 Calendar。Calendar 在 date(from:) 转换时可能会返回 nil,如果组件构成了不存在的日期——例如,2 月 31 日或非闰年的 2 月 30 日。日期验证是 Calendar 的责任,而不是 DateComponents 的责任。
Calendar.date(from:)——将 DateComponents 转换为 Date 的主要方法。Calendar 根据其日历和时区解释这些组件。如果某些字段未设置(nil),Calendar 将使用默认值:日 = 1,小时 = 0,分钟 = 0,秒 = 0。
该方法返回一个可选的 Date——当组件相互矛盾或形成无效日期时会出现 nil。nil 的典型原因:不存在的日期(1 月 32 日、2023 年 2 月 29 日)、矛盾的字段(一组中 weekday=1、day=5)、给定日历不可能出现的年份(公历中年份 0)。
带 timeZone 的 DateComponents——如果 DateComponents 包含 timeZone,Calendar 在转换时会使用它。如果未指定 timeZone,Calendar 使用自己的当前 timeZone。如果 Calendar.timeZone 与日期的预期时区不匹配,结果可能会相差几个小时——请确保在其中一个对象中明确设置了 timeZone。
let calendar = Calendar(identifier: .gregorian)
// 从 DateComponents 创建 Date
var comps = DateComponents()
comps.year = 2026
comps.month = 12
comps.day = 25
comps.hour = 10
if let date = calendar.date(from: comps) {
print("Christmas: \(date)")
}
// 指定 timeZone 创建
calendar.timeZone = TimeZone(identifier: "UTC")!
let utcComps = DateComponents(
calendar: calendar, year: 2026, month: 7, day: 21,
hour: 12
)
let utcDate = calendar.date(from: utcComps)!
Calendar.dateComponents 用于日期差异——DateComponents 的另一个使用场景。Calendar.dateComponents([.year, .month, .day], from: Date(), to: futureDate) 返回两个日期之间的年、月和日差异。这是计算年龄的正确方法,而不是将 TimeInterval 除以一年的秒数,因为 Calendar 会考虑闰年。
Calendar——处理 DateComponents 的核心类。所有日期的提取、组装和比较操作都通过 Calendar 进行。没有 Calendar,DateComponents 只是一组没有时间含义的数字。Calendar 为组件提供解释:确定月份 2 是二月,weekday 2 是星期一。
Calendar.nextDate 和 Calendar.enumerateDates——基于 DateComponents 的两种方法。nextDate(after: Date(), matching: DateComponents) 查找匹配指定组件的下一个日期——例如,今天之后的下一个星期一。enumerateDates(startingAfter:matching:matchingPolicy:using:) 遍历所有匹配模式直到指定限制的日期。
Calendar.dateInterval——为指定组件返回 DateInterval 的方法。dateInterval(of: .month, for: Date()) 返回当前月份的开始和结束。在内部,此方法使用 DateComponents 来查找期间的边界:使用月份的第一天和最后一天创建 DateComponents,然后通过 Calendar 将它们转换为 Date。
let calendar = Calendar.current
// 下周一
let nextMonday = calendar.nextDate(
after: Date(),
matching: DateComponents(weekday: 2),
matchingPolicy: .nextTime
)!
// 日期之间的天数差
let diff = calendar.dateComponents(
[.day], from: Date(), to: nextMonday
)
// 月份范围
let monthInterval = calendar.dateInterval(
of: .month, for: Date()
)!
let startOfMonth = monthInterval.start
let endOfMonth = monthInterval.end
MatchingPolicy——使用 DateComponents 时 Calendar 方法的一个重要参数。strictPolicy 要求所有组件精确匹配,nextTimePolicy 选择下一个时间匹配项,nextTimePreservingSmallerComponents 保留原始日期中的较小组件(分钟、秒)。策略的选择会影响日期搜索结果,特别是在通过夏令时/冬令时切换进行偏移时。
我们将探讨在应用程序中使用 DateComponents 的实际场景。每个示例都演示了 iOS 开发人员在处理日历日期时遇到的典型任务。
Calendar.nextDate 与 DateComponents(day: 1) 查找下个月的第一天。Calendar 自动确定当前月份的天数并转到下个月。对于重复通知,请使用 enumerateDates 或带有 Calendar 键的 Combine.Timer。
func firstDayOfNextMonth(from date: Date) -> Date {
let calendar = Calendar.current
let comps = DateComponents(day: 1)
return calendar.nextDate(
after: date,
matching: comps,
matchingPolicy: .nextTime
)!
}
// 计算年龄(岁)
func ageInYears(from birthDate: Date) -> Int {
let calendar = Calendar.current
let ageComponents = calendar.dateComponents(
[.year], from: birthDate, to: Date()
)
return ageComponents.year ?? 0
}
// 按年和月分组事件
func groupEventsByMonth(_ events: [Event]) -> [String: [Event]] {
let calendar = Calendar.current
return Dictionary(grouping: events) { event in
let comps = calendar.dateComponents(
[.year, .month], from: event.date
)
return "\(comps.year!)-\(comps.month!)"
}
}
计算年龄 通过 Calendar.dateComponents([.year], from:to:)——唯一考虑闰年的正确方法。基于 TimeInterval 的计算(秒 / 31536000)对于 2 月 29 日出生的人会产生错误。Calendar 正确确定生日是否在当前年,并返回准确的年龄。
按年和月分组——用于历史记录或日历屏幕的常见任务。DateComponents 充当分组键:从事件日期中提取年和月,形成一个字符串键,然后通过 Dictionary(grouping:) 进行分组。对于显示,使用带有 “LLLL yyyy” 模板的 DateFormatter 来表示本地化的月份名称。
| 任务 | Calendar 方法 | DateComponents 角色 |
|---|---|---|
| 月份的第一天 | nextDate(after:matching:) | day: 1 |
| 计算年龄 | dateComponents(from:to:) | [.year] 来自差异 |
| 日期分组 | dateComponents(_:from:) | year + month 键 |
| 查找星期几 | nextDate(after:matching:) | weekday: N |
常见问题
原因:不存在的日期(4 月 31 日)、矛盾的字段(weekday=1 与 day=5)、所选日历的无效字段组合。Calendar 尝试在其系统中解释组件——如果组合不可能,结果为 nil。在转换时始终使用 guard let 或 if let。
可以,通过 == 运算符。DateComponents 实现了 Equatable,比较所有字段。如果两个结构体的所有字段都相等,则它们相等(nil == nil 被视为真)。要仅比较部分字段——通过 Calendar.dateComponents 提取相同的集合。
Date——一个绝对的时间点,与日历无关。DateComponents——一组人类可读的数字(年、月、日),仅在 Calendar 的上下文中有意义。Date 可以比较、相减、序列化为 ISO 8601。DateComponents——用于与日历交互的中间表示。
只设置 year 和 month 字段,其余保持 nil。通过 Calendar.date(from:) 转换为 Date 时,Calendar 会自动设置日 = 1,小时 = 0,分钟 = 0。结果——对应于指定月份第一天午夜时分的 Date。
DateComponents 不在字段中存储时区信息——字段的值(年、月、日)本身取决于它们被提取时的 timeZone。“2026 年 7 月 21 日 14:00 MSK” 和 “2026 年 7 月 21 日 10:00 UTC” 的组件表示相同的 Date,但 DateComponents 的字段不同。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。