Calendar คือคลาสของ Foundation ที่กำหนดระบบปฏิทินและให้วิธีการสำหรับการคำนวณทางปฏิทิน เช่น การดึงส่วนประกอบของวันที่ การคำนวณความแตกต่างระหว่างวันที่ การค้นหาขอบเขตของช่วงเวลา และการเลื่อนวันที่ ปฏิทินเชื่อมโยงเวลาสัมบูรณ์ (Date) กับส่วนประกอบที่มนุษย์อ่านได้ และคำนึงถึงความแตกต่างของแต่ละภูมิภาค เช่น วันแรกของสัปดาห์ เขตเวลา และเวลาออมแสง ตามข้อมูลของ Apple Developer Documentation (2025) Foundation รองรับระบบปฏิทิน 17 ระบบ ตั้งแต่เกรกอเรียนไปจนถึงพุทธศักราชและญี่ปุ่น ทำให้ Calendar เป็นเครื่องมืออเนกประสงค์สำหรับแอปพลิเคชันที่รองรับหลายภาษา
สิ่งที่สำคัญ
Calendar คือคลาสของ Foundation ที่ใช้คำนวณทางปฏิทินบนพื้นฐานของ ICU (International Components for Unicode) ปฏิทินกำหนดว่าเวลาสัมบูรณ์ (Date) แสดงเป็นส่วนประกอบของปฏิทินอย่างไร เช่น ปี เดือน วัน ชั่วโมง นาที วินาที หากไม่มี Calendar จะไม่สามารถทราบได้ว่าวันนี้เป็นปี เดือน และวันอะไร เพราะ Date เองไม่มีข้อมูลนี้
ปฏิทินคำนึงถึงพารามิเตอร์สามกลุ่ม: ระบบปฏิทิน (เกรกอเรียน พุทธศักราช ญี่ปุ่น) เขตเวลา และ locale Calendar.current รวมทั้งสามอย่างจากการตั้งค่าระบบของผู้ใช้ Calendar.autoupdatingCurrent เป็นเวอร์ชันพิเศษที่จะอัปเดตอัตโนมัติเมื่อการตั้งค่าเปลี่ยนแปลงโดยไม่ต้องรีสตาร์ทแอปผ่าน NotificationCenter
ปฏิทินเป็น ออบเจกต์แบบ value (value type) ใน Foundation Calendar(identifier:) สร้างอินสแตนซ์ใหม่ที่มีพารามิเตอร์คงที่ Calendar สามารถคัดลอก เปรียบเทียบด้วย == และใช้เป็นคีย์ใน dictionary ได้ ทำให้สามารถสร้างปฏิทินที่มีการตั้งค่า timeZone และ locale เฉพาะสำหรับการทดสอบ
Calendar คือเวอร์ชัน Swift ของ NSCalendar ใน Objective-C ซึ่งเชื่อมต่อกันด้วย as Calendar / as NSCalendar ใน Swift สมัยใหม่จะใช้ Calendar ทุกที่ NSCalendar ยังคงไว้สำหรับความเข้ากันได้ย้อนหลังกับ API ของ Objective-C Calendar มีชุดเมธอดครบถ้วนโดยไม่มีคำนำหน้า NS พร้อมอาร์กิวเมนต์แบบ type-safe และ Swift optionality
Thread Safety — Calendar ปลอดภัยต่อเธรดสำหรับการอ่าน อินสแตนซ์ที่สร้างแล้วสามารถอ่านจากหลายเธรดได้อย่างปลอดภัย การแก้ไขคุณสมบัติ (timeZone, locale) ไม่ปลอดภัยต่อเธรด — สำหรับการกำหนดค่าที่แตกต่างกันควรสร้างอินสแตนซ์ Calendar แยกกัน
Foundation รองรับ ระบบปฏิทิน 17 ระบบ ผ่าน enum Calendar.Identifier แต่ละระบบมีกฎปีอธิกสุรทิน จำนวนเดือน และจุดเริ่มต้นของศักราชที่แตกต่างกัน การเลือกปฏิทินส่งผลต่อการคำนวณทั้งหมด: 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:) สร้าง Date จาก DateComponents โดยเติมค่าฟิลด์ที่เว้นไว้ด้วยค่าเริ่มต้น: วัน = 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 เป็นหนึ่งในเมธอดที่มีประโยชน์ที่สุดสำหรับ analytics และ 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 ใน locale รัสเซียสัปดาห์เริ่มต้นด้วยวันจันทร์ ในอเมริกัน — วันอาทิตย์
Calendar.availableIdentifiers คืนค่ารายการของตัวระบุปฏิทินที่รองรับทั้งหมด static property Calendar.availableCalendarIdentifiers — อาร์เรย์ของสตริงที่มีตัวระบุเดียวกัน ใช้สำหรับสร้าง UI เลือกปฏิทินและตรวจสอบความพร้อมใช้งานของระบบปฏิทินเฉพาะบนอุปกรณ์
// Calendar ที่มีเขตเวลาเฉพาะ
var utcCalendar = Calendar(identifier: .gregorian)
utcCalendar.timeZone = TimeZone(identifier: "UTC")!
// Calendar ที่มี locale รัสเซีย
var russianCalendar = Calendar(identifier: .gregorian)
russianCalendar.locale = Locale(identifier: "ru_RU")
// วันแรกของสัปดาห์ขึ้นอยู่กับ locale
let firstWeekday = russianCalendar.firstWeekday
// 2 = วันจันทร์ (ใน ru_RU)
// รายการปฏิทินที่พร้อมใช้งาน
for identifier in Calendar.availableIdentifiers {
print(identifier)
}
firstWeekday — คุณสมบัติของ Calendar ที่กำหนดว่าวันใดของสัปดาห์ถือเป็นวันแรก ใน locale รัสเซีย Sunday = 2 (วันจันทร์เป็นวันแรก) ในอเมริกัน Sunday = 1 ซึ่งส่งผลต่อการทำงานของ weekOfMonth และ weekOfYear: วันที่เดียวกันอาจถูกจัดให้อยู่ในหมายเลขสัปดาห์ที่ต่างกันในแต่ละ locale สำหรับแอปที่ใช้งานวันที่ ใช้ 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) คืนค่า 1..<32 สำหรับเดือนที่มี 31 วัน หรือ 1..<29 สำหรับเดือนกุมภาพันธ์ของปีที่ไม่ใช่อธิกสุรทิน นี่คือวิธีที่ถูกต้องในการหาจำนวนวันในเดือน ไม่ใช่การเขียนค่าแบบ hardcode
การเพิ่มเดือน ผ่าน Calendar.date(byAdding:value:to:) จัดการกับวันที่ปลายขอบอย่างถูกต้อง หากเพิ่ม 1 เดือนจากวันที่ 31 มกราคม Calendar จะคืนค่าวันที่ 28 กุมภาพันธ์ (หรือ 29 ในปีอธิกสุรทิน) ไม่ใช่ 3 มีนาคมอย่างที่ได้จากการบวก 30 วันผ่าน TimeInterval นี่เป็นอีกเหตุผลที่ไม่ควรใช้ TimeInterval สำหรับการคำนวณทางปฏิทิน
| เมธอดของ Calendar | การใช้งาน | ตัวอย่าง |
|---|---|---|
| dateInterval | ขอบเขตของช่วงเวลา | จุดเริ่มต้นและสิ้นสุดของเดือน |
| range(of:in:for:) | ช่วงของส่วนประกอบ | วันในเดือนปัจจุบัน |
| date(byAdding:) | เลื่อนวันที่ | +1 เดือนจากวันนี้ |
| isDateInToday | ตรวจสอบว่าวันนี้ | วันที่เกี่ยวข้องกับวันนี้หรือไม่ |
| compare(toGranularity:) | เปรียบเทียบด้วยความแม่นยำ | วันเดียวกันโดยไม่คำนึงถึงเวลา |
คำถามที่พบบ่อย
Calendar.current คืนค่าปฏิทินจากการตั้งค่าระบบของผู้ใช้ — อาจไม่ใช่เกรกอเรียน (เช่น พุทธศักราชในประเทศไทย) Calendar(identifier: .gregorian) สร้างปฏิทินเกรกอเรียนเสมอโดยไม่ขึ้นกับการตั้งค่า สำหรับการแสดงวันที่ให้ใช้ Calendar.current สำหรับตรรกะทางธุรกิจให้ใช้ตัวระบุที่เลือกอย่างชัดเจน
สาเหตุมาจากความยาวของเดือนที่แตกต่างกัน หากวันที่ปัจจุบันเป็น 31 มกราคม การเพิ่ม 1 เดือนจะได้ 28 กุมภาพันธ์ เพราะเดือนกุมภาพันธ์ไม่มีวันที่ 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: year, month: 2, day: 29)) != nil — หากมีวันที่ 29 กุมภาพันธ์ แสดงว่าเป็นปีอธิกสุรทิน Calendar คำนวณกฎของแต่ละระบบปฏิทินให้เอง
ได้ คุณสมบัติ firstWeekday เขียนได้ การเปลี่ยนแปลงมีผลต่อ weekOfMonth, weekOfYear และการคำนวณทั้งหมดที่เกี่ยวข้องกับหมายเลขสัปดาห์ เมื่อกำหนด locale = Locale(identifier: "ru_RU") firstWeekday จะกลายเป็น 2 (วันจันทร์) โดยอัตโนมัติ การตั้งด้วยมือจะแทนค่าจาก locale
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม