DateComponents — 这是什么,日历组件和 NSCalendar

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

DateComponents 是一个 Foundation 结构体,它将日历日期的各个组件存储为单独的字段:年、月、日、小时、分钟、秒等。与表示绝对时间点的 Date 不同,DateComponents 包含人类可读的值,这些值依赖于日历和时区。根据 Apple Developer Documentation (2025),DateComponents 被用作 Date 和 Calendar 之间的中间环节——通过它来提取和构造日历日期,无需手动运算即可执行日期计算和偏移。

要点

  • DateComponents — 用于将日期组件(年、月、日)存储为可选整数字段的结构体。
  • Calendar.dateComponents — 从 Date 中提取指定组件并考虑时区的方法。
  • Calendar.date(from:) — 将 DateComponents 反向转换为 Date,自动填充缺失字段。
  • 可选字段 — DateComponents 的每个字段都可以为 nil,从而允许指定不完整的日期。
  • Range 和组件 — DateComponents 在 Calendar 中用于计算日期之间的差异以及查找范围内的日期。

什么是 DateComponents?

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, day1..∞, 1..12, 1..31
时间hour, minute, second, nanosecond0..23, 0..59, 0..59, 0..999999999
weekday, weekOfMonth, weekOfYear1..7, 1..5, 1..53
特殊quarter, yearForWeekOfYear1..4, 依赖

通过 Calendar.dateComponents 提取组件时,为了性能只请求必要的字段很重要。Calendar 在单次遍历中提取所有请求的字段——这比单独为每个字段调用 Calendar.component 要快得多。

创建 DateComponents

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。

swift
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 的责任。

将 DateComponents 转换为 Date

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。

swift
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——处理 DateComponents 的核心类。所有日期的提取、组装和比较操作都通过 Calendar 进行。没有 Calendar,DateComponents 只是一组没有时间含义的数字。Calendar 为组件提供解释:确定月份 2 是二月,weekday 2 是星期一。

Calendar.nextDateCalendar.enumerateDates——基于 DateComponents 的两种方法。nextDate(after: Date(), matching: DateComponents) 查找匹配指定组件的下一个日期——例如,今天之后的下一个星期一。enumerateDates(startingAfter:matching:matchingPolicy:using:) 遍历所有匹配模式直到指定限制的日期。

Calendar.dateInterval——为指定组件返回 DateInterval 的方法。dateInterval(of: .month, for: Date()) 返回当前月份的开始和结束。在内部,此方法使用 DateComponents 来查找期间的边界:使用月份的第一天和最后一天创建 DateComponents,然后通过 Calendar 将它们转换为 Date。

swift
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 示例

我们将探讨在应用程序中使用 DateComponents 的实际场景。每个示例都演示了 iOS 开发人员在处理日历日期时遇到的典型任务。

每月第一天的提醒

Calendar.nextDate 与 DateComponents(day: 1) 查找下个月的第一天。Calendar 自动确定当前月份的天数并转到下个月。对于重复通知,请使用 enumerateDates 或带有 Calendar 键的 Combine.Timer。

swift
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

常见问题

为什么 Calendar.date(from:) 对 DateComponents 返回 nil?

原因:不存在的日期(4 月 31 日)、矛盾的字段(weekday=1 与 day=5)、所选日历的无效字段组合。Calendar 尝试在其系统中解释组件——如果组合不可能,结果为 nil。在转换时始终使用 guard letif let

DateComponents 之间可以相互比较吗?

可以,通过 == 运算符。DateComponents 实现了 Equatable,比较所有字段。如果两个结构体的所有字段都相等,则它们相等(nil == nil 被视为真)。要仅比较部分字段——通过 Calendar.dateComponents 提取相同的集合。

DateComponents 与 Date 有什么区别?

Date——一个绝对的时间点,与日历无关。DateComponents——一组人类可读的数字(年、月、日),仅在 Calendar 的上下文中有意义。Date 可以比较、相减、序列化为 ISO 8601。DateComponents——用于与日历交互的中间表示。

如何在 DateComponents 中只指定年和月?

只设置 year 和 month 字段,其余保持 nil。通过 Calendar.date(from:) 转换为 Date 时,Calendar 会自动设置日 = 1,小时 = 0,分钟 = 0。结果——对应于指定月份第一天午夜时分的 Date。

DateComponents 如何处理时区?

DateComponents 不在字段中存储时区信息——字段的值(年、月、日)本身取决于它们被提取时的 timeZone。“2026 年 7 月 21 日 14:00 MSK” 和 “2026 年 7 月 21 日 10:00 UTC” 的组件表示相同的 Date,但 DateComponents 的字段不同。

总结

  • DateComponents——用于将日历组件(年、月、日、小时)存储为可选 Int? 字段的 Foundation 结构体。
  • Calendar.dateComponents 考虑时区和日历系统从 Date 中提取组件。
  • Calendar.date(from:) 使用默认值从 DateComponents 组装 Date 以填补缺失字段。
  • 字段的可选性允许指定不完整的日期——Calendar 补充缺失的值。
  • Calendar.nextDate 搜索匹配 DateComponents 的下一个日期——用于提醒和重复事件。
  • 计算年龄 通过 Calendar.dateComponents([.year], from:to:)——唯一考虑闰年的正确方法。
  • MatchingPolicy 控制当所有组件不匹配时 Calendar 的行为——搜索日期的重要参数。

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

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

讨论项目

另请阅读