Calendar là một lớp Foundation định nghĩa hệ thống lịch và cung cấp các phương thức cho tính toán lịch: trích xuất thành phần ngày, tính hiệu giữa các ngày, tìm ranh giới khoảng thời gian và dời chuyển ngày. Lịch kết nối thời gian tuyệt đối (Date) với các thành phần có thể đọc được bởi con người và tính đến các đặc điểm khu vực: ngày đầu tuần, múi giờ và giờ mùa hè. Theo Tài liệu Nhà phát triển Apple (2025), Foundation hỗ trợ 17 hệ thống lịch — từ Gregory đến Phật lịch và Nhật lịch — làm cho Calendar trở thành công cụ phổ quát cho các ứng dụng quốc tế hóa.
Các điểm chính
Calendar là một lớp Foundation thực hiện các tính toán lịch dựa trên ICU (International Components for Unicode). Lịch xác định cách thời gian tuyệt đối (Date) ánh xạ tới các thành phần lịch: năm, tháng, ngày, giờ, phút, giây. Không có Calendar thì không thể biết được hôm nay là năm nào, tháng nào, ngày nào — bản thân Date không chứa thông tin này.
Lịch tính đến ba nhóm tham số: hệ thống lịch (Gregory, Phật lịch, Nhật lịch), múi giờ và khu vực. Calendar.current kết hợp cả ba từ cài đặt hệ thống của người dùng. Calendar.autoupdatingCurrent là một phiên bản đặc biệt tự động cập nhật khi thay đổi cài đặt mà không cần khởi động lại ứng dụng thông qua NotificationCenter.
Calendar là một kiểu giá trị (value type) trong Foundation. Calendar(identifier:) tạo một thực thể mới với các tham số cố định. Calendar có thể được sao chép, so sánh qua == và sử dụng làm khóa trong từ điển. Điều này cho phép tạo các lịch với cài đặt timeZone và locale cụ thể để thử nghiệm.
Calendar là phiên bản Swift của NSCalendar trong Objective-C, được cầu nối qua as Calendar / as NSCalendar. Trong Swift hiện đại, Calendar được sử dụng ở khắp mọi nơi. NSCalendar vẫn tồn tại để tương thích ngược với các API Objective-C. Calendar có một bộ phương thức đầy đủ không có tiền tố NS, với các đối số type-safe và tùy chọn Swift.
An toàn luồng — Calendar an toàn cho việc đọc từ nhiều luồng. Một thực thể đã tạo có thể được đọc an toàn từ nhiều luồng. Việc sửa đổi thuộc tính (timeZone, locale) không an toàn cho luồng — hãy tạo các thực thể Calendar riêng biệt cho các cấu hình khác nhau.
Foundation hỗ trợ 17 hệ thống lịch thông qua liệt kê Calendar.Identifier. Mỗi hệ thống có các quy tắc riêng về năm nhuận, số tháng và bắt đầu kỷ nguyên. Việc chọn lịch ảnh hưởng đến tất cả các tính toán: dateComponents, dateInterval, nextDate.
Các hệ thống lịch chính:
Calendar(identifier: .gregorian) — được sử dụng nhiều nhất. Nó tuân thủ tiêu chuẩn quốc tế ISO 8601 và là lịch mặc định ở hầu hết các quốc gia. Đối với các ứng dụng có khán giả quốc tế, hãy sử dụng Calendar.current — nó tự động khớp với lịch hệ thống của người dùng.
| Mã nhận dạng | Loại | Khu vực sử dụng |
|---|---|---|
| .gregorian | Dương lịch | Quốc tế |
| .buddhist | Dương lịch | Thái Lan, Campuchia |
| .japanese | Dương lịch | Nhật Bản |
| .hebrew | Âm dương lịch | Israel |
| .islamic | Âm lịch | Các nước Hồi giáo |
| .chinese | Âm dương lịch | Trung Quốc |
DateComponents và Calendar là một cặp không thể tách rời. Calendar.dateComponents(_:from:) trích xuất các thành phần từ Date tôn trọng múi giờ của lịch. Calendar.date(from:) tổng hợp Date từ DateComponents, điền các trường thiếu bằng giá trị mặc định: ngày = 1, giờ = 0, phút = 0, giây = 0.
Phương thức Calendar.component trích xuất một thành phần duy nhất, thuận tiện cho việc kiểm tra nhanh. Calendar.dateComponents trích xuất nhiều thành phần trong một lần gọi — điều này hiệu quả hơn vì Calendar thực hiện tính toán lịch một lần thay vì cho từng thành phần riêng lẻ. Đối với danh sách 3+ thành phần, luôn sử dụng dateComponents.
Calendar.compare so sánh hai Date với độ chính xác xác định. Tham số toGranularity xác định độ chính xác của thành phần: .year chỉ so sánh năm, .month — năm và tháng, .day — năm, tháng, ngày. Điều này hữu ích để kiểm tra xem hai ngày có cùng một ngày không, bỏ qua thời gian.
let calendar = Calendar.current
let now = Date()
// Trích xuất một thành phần
let year = calendar.component(.year, from: now)
// Trích xuất một tập hợp thành phần
let comps = calendar.dateComponents(
[.year, .month, .day], from: now
)
// So sánh với độ chính xác ngày
let isSameDay = calendar.compare(date1, to: date2,
toGranularity: .day) == .orderedSame
// Kiểm tra ngày có phải hôm nay không
let isToday = calendar.isDateInToday(someDate)
Calendar.isDateInToday, isDateInTomorrow, isDateInYesterday — các phương thức cho kiểm tra tương đối. Calendar.isDate(_:inSameDayAs:) kiểm tra xem hai ngày có rơi vào cùng một ngày lịch hay không, có tính đến múi giờ của lịch. Các phương thức này sử dụng Calendar.compare bên trong và được tối ưu cho các lần gọi thường xuyên.
Calendar.dateInterval là một trong những phương thức hữu ích nhất cho phân tích và giao diện người dùng. Nó trả về DateInterval cho thành phần chỉ định: bắt đầu và kết thúc của một ngày, tuần, tháng, năm. DateInterval chứa start (Date) và end (Date) — ranh giới của khoảng thời gian. Ví dụ, dateInterval(of: .weekOfYear, for: Date()) trả về ngày bắt đầu thứ Hai và kết thúc Chủ nhật của tuần hiện tại.
Calendar.date với byAdding — một phương thức để dời chuyển ngày. Calendar.date(byAdding: .day, value: 7, to: Date()) trả về ngày sau một tuần. Calendar.date(byAdding: DateComponents) là một phiên bản linh hoạt hơn cho phép dời chuyển nhiều thành phần cùng một lúc: +1 tháng +3 ngày. Calendar tự động tính đến độ dài tháng khác nhau và năm nhuận.
Calendar.nextDate tìm ngày tiếp theo khớp với DateComponents chỉ định. Tham số matchingPolicy xác định hành vi khi không khớp: .nextTime — lần khớp tiếp theo về thời gian, .nextTimePreservingSmallerComponents — giữ nguyên phút và giây từ ngày gốc, .strict — yêu cầu khớp chính xác.
let calendar = Calendar.current
let today = Date()
// Bắt đầu và kết thúc tuần
let weekInterval = calendar.dateInterval(
of: .weekOfYear, for: today
)!
// Dời chuyển 1 tháng
let nextMonth = calendar.date(
byAdding: .month, value: 1, to: today
)!
// Dời chuyển qua DateComponents
var delta = DateComponents()
delta.month = 1
delta.day = 3
let shifted = calendar.date(byAdding: delta, to: today)!
// Thứ Sáu ngày 13 tiếp theo
let friday13Components = DateComponents(
weekday: 6, day: 13
)
let nextFriday13 = calendar.nextDate(
after: today, matching: friday13Components,
matchingPolicy: .nextTime
)
EnumerateDates — một phương thức mạnh mẽ để lặp qua các ngày theo mẫu. Calendar.enumerateDates(startingAfter:matching:matchingPolicy:using:) gọi một khối cho mỗi lần khớp cho đến khi khối trả về stop = true. Được sử dụng để tạo các sự kiện lặp lại trong lịch và lịch trình. Phương thức này hiệu quả hơn so vòng lặp thủ công với nextDate vì nó được ICU tối ưu hóa.
TimeZone là một phần không thể thiếu của Calendar. Múi giờ xác định thời gian lịch tương ứng với một Date tuyệt đối. Cùng một Date ở UTC và ở Moscow cho các thành phần khác nhau: Date() ở UTC có thể hiển thị 10:00, trong khi ở MSK — 13:00. Calendar.timeZone mặc định là TimeZone.current.
Locale ảnh hưởng đến ngày đầu tuần, số ngày tối thiểu trong tuần đầu tiên của năm (minDaysInFirstWeek) và tên tháng/ngày trong tuần (khi chuyển đổi qua DateFormatter). Calendar.locale mặc định là Locale.current. Trong khu vực tiếng Nga, tuần bắt đầu vào thứ Hai, trong khu vực Mỹ — vào Chủ nhật.
Calendar.availableIdentifiers trả về danh sách tất cả các mã nhận dạng lịch được hỗ trợ. Thuộc tính tĩnh Calendar.availableCalendarIdentifiers là một mảng các chuỗi với các mã nhận dạng giống nhau. Được sử dụng để xây dựng giao diện chọn lịch và kiểm tra tính khả dụng của một hệ thống lịch cụ thể trên thiết bị.
// Calendar với múi giờ cụ thể
var utcCalendar = Calendar(identifier: .gregorian)
utcCalendar.timeZone = TimeZone(identifier: "UTC")!
// Calendar với khu vực tiếng Nga
var russianCalendar = Calendar(identifier: .gregorian)
russianCalendar.locale = Locale(identifier: "ru_RU")
// Ngày làm việc đầu tiên phụ thuộc vào khu vực
let firstWeekday = russianCalendar.firstWeekday
// 2 = Thứ Hai (trong ru_RU)
// Danh sách lịch có sẵn
for identifier in Calendar.availableIdentifiers {
print(identifier)
}
firstWeekday — một thuộc tính Calendar xác định ngày nào trong tuần được coi là ngày đầu tiên. Trong khu vực tiếng Nga, Sunday = 2 (thứ Hai là đầu tiên). Trong khu vực Mỹ, Sunday = 1. Điều này ảnh hưởng đến weekOfMonth và weekOfYear: cùng một ngày có thể thuộc về số tuần khác nhau trong các khu vực khác nhau. Đối với các ứng dụng làm việc với ngày tháng, hãy sử dụng Calendar.current hoặc đặt firstWeekday một cách rõ ràng.
Hãy xem xét các kịch bản thực tế minh họa khả năng của Calendar. Mỗi ví dụ giải quyết một tác vụ phát triển iOS cụ thể và cho thấy cách sử dụng đúng các tính toán lịch.
Calendar.dateInterval(of: .month, for:) trả về ranh giới của tháng hiện tại. Kiểm tra xem một Date có nằm trong khoảng này không là cách nhanh nhất để xác định một ngày có thuộc tháng hiện tại hay không. Một cách khác là Calendar.compare với độ chính xác .month: nếu kết quả là .orderedSame thì tháng khớp.
func isInCurrentMonth(_ date: Date) -> Bool {
let calendar = Calendar.current
let monthInterval = calendar.dateInterval(
of: .month, for: Date()
)!
return monthInterval.contains(date)
}
// Số ngày trong một tháng
func daysInMonth(for date: Date) -> Int {
let calendar = Calendar.current
return calendar.range(
of: .day, in: .month, for: date
)?.count ?? 0
}
// Thêm tháng với điều chỉnh chính xác
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:) trả về phạm vi các giá trị hợp lệ cho một thành phần được chỉ định trong bối cảnh của một thành phần khác. Ví dụ, range(of: .day, in: .month, for: date) trả về 1..<32 cho các tháng có 31 ngày hoặc 1..<29 cho tháng Hai của năm không nhuận. Đây là cách đúng để biết số ngày trong một tháng, thay vì sử dụng các giá trị cứng.
Thêm tháng qua Calendar.date(byAdding:value:to:) xử lý đúng các ngày biên. Nếu bạn thêm 1 tháng vào ngày 31 tháng 1, Calendar trả về ngày 28 tháng 2 (hoặc 29 trong năm nhuận), thay vì ngày 3 tháng 3 sẽ xảy ra nếu chỉ thêm 30 ngày qua TimeInterval. Đây là một lý do nữa để không sử dụng TimeInterval cho các tính toán lịch.
| Phương thức Calendar | Mục đích | Ví dụ |
|---|---|---|
| dateInterval | Ranh giới khoảng thời gian | Bắt đầu và kết thúc tháng |
| range(of:in:for:) | Phạm vi thành phần | Ngày trong tháng hiện tại |
| date(byAdding:) | Dời chuyển ngày | +1 tháng từ hôm nay |
| isDateInToday | Kiểm tra hôm nay | Ngày có phải hôm nay không? |
| compare(toGranularity:) | So sánh với độ chính xác | Cùng ngày bỏ qua thời gian |
Câu hỏi thường gặp
Calendar.current trả về lịch từ cài đặt hệ thống của người dùng — nó có thể không phải Gregory (ví dụ, Phật lịch ở Thái Lan). Calendar(identifier: .gregorian) luôn tạo lịch Gregory bất kể cài đặt. Sử dụng Calendar.current để hiển thị ngày và mã nhận dạng được chọn rõ ràng cho logic nghiệp vụ.
Điều này liên quan đến độ dài tháng khác nhau. Nếu ngày hiện tại là 31 tháng 1, thêm 1 tháng sẽ cho ngày 28 tháng 2, vì tháng 2 không có ngày 31. Calendar tự động điều chỉnh ngày đến ngày hợp lệ cuối cùng của tháng. Để kiểm soát chính xác, hãy sử dụng DateComponents với day: 1 để chuyển đến ngày đầu tiên của tháng.
DateFormatter sử dụng Calendar.current — lịch hệ thống của người dùng. Nếu một ứng dụng luôn hiển thị ngày theo lịch Gregory bất kể cài đặt, hãy đặt formatter.calendar = Calendar(identifier: .gregorian). Điều này đảm bảo hiển thị thống nhất cho tất cả người dùng.
Calendar.range(of: .day, in: .year, for: date) trả về 365 hoặc 366 ngày. Đơn giản hơn: Calendar.date(from: DateComponents(year: year, month: 2, day: 29)) != nil — nếu ngày 29 tháng 2 tồn tại, năm đó là năm nhuận. Calendar tự động xử lý các quy tắc cho hệ thống lịch cụ thể.
Có, thuộc tính firstWeekday có thể ghi. Thay đổi ảnh hưởng đến weekOfMonth, weekOfYear và tất cả các tính toán liên quan đến số tuần. Khi đặt locale = Locale(identifier: “ru_RU”), firstWeekday tự động trở thành 2 (thứ Hai). Cài đặt thủ công ghi đè giá trị từ locale.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm