前言
在 iOS 开发中,实现一个日历组件几乎是每个带日期选择功能的 App 的必修课。但很多开发者对 Date、Calendar、DateComponents 这三个核心对象的认知是模糊的,导致写出来的日历代码逻辑混乱。你有没有遇到过这样的情况:想获取"这个月第一天是周几",结果用 Date 加加减减算出来的结果总是不对?或者在处理跨月、跨年时,代码莫名其妙就崩了?这些问题的根源,往往是对 Date、Calendar、DateComponents 这三个对象的职责边界认识不清。这篇文章带你理解这三个对象,然后用它们亲手实现一个完整的日历组件。
读完这篇文章,你会得到:
彻底理解 Date、Calendar、DateComponents的定位与关系掌握日历计算的核心逻辑(每月有多少天、第一天是周几、当月选择、上下月切换) 一份可直接运行的完整日历代码
第一部分:三个核心对象的本质
Date:时间轴上的一个点
Date 本质上是一个 Double 类型的秒数(TimeInterval),表示从2001年1月1日 00:00:00 UTC到某个时刻所经过的时间。
let now = Date()print(now.timeIntervalSinceReferenceDate) // 807657800.123456(浮点数)
关键认知:
Date不包含时区、日历、年月日信息 它只是一个"刻度",在全世界任何一个时区下,同一个 Date的值都相同它本身不可读,需要借助 Calendar转换成人类能理解的年月日可以把
Date想象成一根尺子上唯一的刻度,这个刻度是绝对的,但它本身没有"标签"。
DateComponents:"年月日时分秒"的数据容器
DateComponents 只是一个结构体,包含了年、月、日、时、分、秒等可选字段。
var comps = DateComponents()comps.year = 2026comps.month = 8comps.day = 9comps.hour = 15comps.minute = 30
关键认知:
它只是一组数字,不一定能组成有效日期(比如 month=13、day=32都是合法的DateComponents,但转成Date会失败)它本身不依赖日历,但转成 Date时必须通过Calendar精度支持到纳秒( nanosecond字段),微秒和皮秒不支持
Calendar:连接"刻度"与"标签"的桥梁
Calendar 负责在 Date(时间轴上的点)和 DateComponents(年月日数字)之间进行转换,并执行基于日历的数学运算(加减日期、计算间隔、获取月初月末等)。
let calendar = Calendar.currentlet now = Date()// Date → DateComponents(拆解)let comps = calendar.dateComponents([.year, .month, .day], from: now)print(”今天是 \(comps.year!)-\(comps.month!)-\(comps.day!)”)// DateComponents → Date(组装)var birthdayComps = DateComponents()birthdayComps.year = 1990birthdayComps.month = 1birthdayComps.day = 1let birthday = calendar.date(from: birthdayComps)
关键认知:
Calendar是时区敏感的,同一个 Date在不同时区的Calendar下解析出的年月日不同Calendar是日历计算的唯一正确方式,不要手动做秒数加减(会踩夏令时的坑)
三者关系总览
”2026年8月9日”↑↓DateComponents { year: 2026, month: 8, day: 9 }↑↓Calendar(拆解/组装/计算)↑↓Date(时间轴上的点,存储为秒数)
总结
对象 本质 职责 Date绝对时间点(秒数) 存储绝对时间 DateComponents年月日数字容器 承载人类可读的日期数字 Calendar日历计算引擎 在 Date和DateComponents之间转换,做日期运算
掌握了这三个对象的关系,接下来我们就可以动手实现一个完整的日历组件了。核心逻辑包括:计算每月有多少天、每月第一天是周几、以及上下月切换。
下一篇文章,我们正式开始编码!🚀
夜雨聆风