![](/img/trans.png)
[英]Custom datepicker with UIPickerView first day of the month is always monday
[英]Get first Monday in Calendar month
我被困在日歷/時區/日期組件/日期地獄中,我的頭腦在旋轉。
我正在創建一個日記應用程序,我希望它在全球范圍內工作(使用 iso8601 日歷)。
我正在嘗試在 iOS 上創建一個類似於 macOS 日歷的日歷視圖,以便我的用戶可以瀏覽他們的日記條目(注意每個月的 7x6 網格):
要獲得本月的第一天,我可以執行以下操作:
func firstOfMonth(month: Int, year: Int) -> Date {
let calendar = Calendar(identifier: .iso8601)
var firstOfMonthComponents = DateComponents()
// firstOfMonthComponents.timeZone = TimeZone(identifier: "UTC") // <- fixes daylight savings time
firstOfMonthComponents.calendar = calendar
firstOfMonthComponents.year = year
firstOfMonthComponents.month = month
firstOfMonthComponents.day = 01
return firstOfMonthComponents.date!
}
(1...12).forEach {
print(firstOfMonth(month: $0, year: 2018))
/*
Gives:
2018-01-01 00:00:00 +0000
2018-02-01 00:00:00 +0000
2018-03-01 00:00:00 +0000
2018-03-31 23:00:00 +0000
2018-04-30 23:00:00 +0000
2018-05-31 23:00:00 +0000
2018-06-30 23:00:00 +0000
2018-07-31 23:00:00 +0000
2018-08-31 23:00:00 +0000
2018-09-30 23:00:00 +0000
2018-11-01 00:00:00 +0000
2018-12-01 00:00:00 +0000
*/
}
這里有一個關於夏令時的直接問題。 可以通過取消注釋注釋行並強制以 UTC 計算日期來“修復”該問題。 我覺得在不同時區查看日歷視圖時,通過強制將其設置為 UTC,日期會變得無效。
真正的問題是:我如何獲得包含該月第一天的一周中的第一個星期一? 例如,我如何獲得 2 月 29 日星期一或 4 月 26 日星期一? (請參閱 macOS 屏幕截圖)。 要到月底,我是否只需從開始的 42 天開始添加? 還是那太天真了?
編輯
感謝當前的答案,但我們仍然被卡住了。
以下工作,直到您考慮夏令時:
好的,這將是一個冗長而復雜的答案(萬歲!)。
總體而言,您走在正確的軌道上,但仍存在一些細微的錯誤。
現在(我希望) Date
代表時間的瞬間已經非常成熟。 不太確定的是相反的情況。 Date
代表一個瞬間,但什么代表日歷值?
當您考慮它時,“天”或“小時”(甚至“月”)是一個范圍。 它們是表示開始時刻和結束時刻之間所有可能時刻的值。
出於這個原因,我發現在處理日歷值時考慮范圍最有幫助。 換句話說,通過詢問“這一分鍾/小時/天/周/月/年的范圍是多少?” 等等。
有了這個,我想你會發現更容易理解發生了什么。 你已經了解Range<Int>
了吧? 所以Range<Date>
同樣直觀。
幸運的是, Calendar
上有 API 可以獲取范圍。 不幸的是,由於 Objective-C 時代的保留 API,我們不能完全使用Range<Date>
。 但是我們得到了NSDateInterval
,這實際上是一回事。
您可以通過向Calendar
詢問包含特定時刻的小時/日/周/月/年范圍來請求范圍,如下所示:
let now = Date()
let calendar = Calendar.current
let today = calendar.dateInterval(of: .day, for: now)
有了這個,你可以得到幾乎任何東西的范圍:
let units = [Calendar.Component.second, .minute, .hour, .day, .weekOfMonth, .month, .quarter, .year, .era]
for unit in units {
let range = calendar.dateInterval(of: unit, for: now)
print("Range of \(unit): \(range)")
}
在我寫這篇文章的時候,它打印:
Range of second: Optional(2018-10-04 19:50:10 +0000 to 2018-10-04 19:50:11 +0000)
Range of minute: Optional(2018-10-04 19:50:00 +0000 to 2018-10-04 19:51:00 +0000)
Range of hour: Optional(2018-10-04 19:00:00 +0000 to 2018-10-04 20:00:00 +0000)
Range of day: Optional(2018-10-04 06:00:00 +0000 to 2018-10-05 06:00:00 +0000)
Range of weekOfMonth: Optional(2018-09-30 06:00:00 +0000 to 2018-10-07 06:00:00 +0000)
Range of month: Optional(2018-10-01 06:00:00 +0000 to 2018-11-01 06:00:00 +0000)
Range of quarter: Optional(2018-10-01 06:00:00 +0000 to 2019-01-01 07:00:00 +0000)
Range of year: Optional(2018-01-01 07:00:00 +0000 to 2019-01-01 07:00:00 +0000)
Range of era: Optional(0001-01-03 00:00:00 +0000 to 139369-07-19 02:25:04 +0000)
您可以看到顯示的各個單位的范圍。 這里有兩點需要指出:
此方法返回一個可選的( DateInterval?
),因為它可能會在奇怪的情況下失敗。 我想不出任何事情,但日歷是變化無常的東西,所以請注意這一點。
要求一個時代的范圍通常是一個荒謬的操作,即使時代對於正確構建日期至關重要。 在某些日歷(主要是日本日歷)之外的時代很難有太多的精確度,所以說“共同時代(公元或公元)開始於第一年的 1 月 3 日”的價值可能是錯誤的,但我們不能真的對此有所作為。 此外,當然,我們不知道當前時代何時結束,所以我們只是假設它永遠持續下去。
Date
值這讓我想到下一點,關於打印日期。 我在您的代碼中注意到了這一點,我認為這可能是您的某些問題的根源。 我贊賞你為解釋夏令時這一令人憎惡的事情所做的努力,但我認為你被一些實際上不是錯誤的東西所吸引,但這是:
Dates
總是以 UTC 打印。
永遠永遠永遠。
這是因為它們是時間上的瞬間,無論您是在加利福尼亞、吉布提、加德滿都還是其他任何地方,它們都是相同的瞬間。 一個Date
值沒有一個時區。 它實際上只是與另一個已知時間點的偏移。 但是,將560375786.836208
打印為Date's
描述對人類來說似乎沒什么用,因此 API 的創建者將其打印為相對於 UTC 時區的公歷日期。
如果你想打印一個日期,即使是為了調試,你幾乎肯定需要一個DateFormatter
。
DateComponents.date
這不是你的代碼的問題,而是更多的提醒。 該.date
物業DateComponents
並不能保證返回指定的組件匹配的第一個瞬間。 它不保證返回與組件匹配的最后時刻。 它所做的只是保證,如果它可以找到答案,它將在指定單位范圍內的某處為您提供Date
。
我不認為您在技術上依賴於您的代碼中的這一點,但了解這一點是件好事,而且我已經看到這種誤解導致了軟件中的錯誤。
考慮到所有這些,讓我們來看看您想要做什么:
因此,根據我們對范圍的了解,現在這應該非常簡單,而且有趣的是,我們可以在不需要單個DateComponents
值的情況下完成所有DateComponents
。 在下面的代碼中,為了簡潔起見,我將強制展開值。 在您的實際代碼中,您可能希望有更好的錯誤處理。
這很簡單:
let yearRange = calendar.dateInterval(of: .year, for: now)!
這有點復雜,但還不錯。
var monthsOfYear = Array<DateInterval>()
var startOfCurrentMonth = yearRange.start
repeat {
let monthRange = calendar.dateInterval(of: .month, for: startOfCurrentMonth)!
monthsOfYear.append(monthRange)
startOfCurrentMonth = monthRange.end.addingTimeInterval(1)
} while yearRange.contains(startOfCurrentMonth)
基本上,我們將從一年中的第一個時刻開始,並在日歷中查詢包含該時刻的月份。 一旦我們得到了那個,我們將獲得那個月的最后時刻,並為其增加一秒以(表面上)將其轉移到下個月。 然后我們將獲得包含該時刻的月份范圍,依此類推。 重復直到我們有一個不再在年份中的值,這意味着我們已經聚合了初始年份的所有月份范圍。
當我在我的機器上運行它時,我得到了這個結果:
[
2018-01-01 07:00:00 +0000 to 2018-02-01 07:00:00 +0000,
2018-02-01 07:00:00 +0000 to 2018-03-01 07:00:00 +0000,
2018-03-01 07:00:00 +0000 to 2018-04-01 06:00:00 +0000,
2018-04-01 06:00:00 +0000 to 2018-05-01 06:00:00 +0000,
2018-05-01 06:00:00 +0000 to 2018-06-01 06:00:00 +0000,
2018-06-01 06:00:00 +0000 to 2018-07-01 06:00:00 +0000,
2018-07-01 06:00:00 +0000 to 2018-08-01 06:00:00 +0000,
2018-08-01 06:00:00 +0000 to 2018-09-01 06:00:00 +0000,
2018-09-01 06:00:00 +0000 to 2018-10-01 06:00:00 +0000,
2018-10-01 06:00:00 +0000 to 2018-11-01 06:00:00 +0000,
2018-11-01 06:00:00 +0000 to 2018-12-01 07:00:00 +0000,
2018-12-01 07:00:00 +0000 to 2019-01-01 07:00:00 +0000
]
同樣,重要的是要在這里指出這些日期是 UTC,即使我在山區時區。 因此,小時在 07:00 和 06:00 之間變化,因為山區時區比 UTC 晚 7 或 6 小時,這取決於我們是否觀察到 DST。 所以這些值對於我日歷的時區是准確的。
這就像之前的代碼:
var daysInMonth = Array<DateInterval>()
var startOfCurrentDay = currentMonth.start
repeat {
let dayRange = calendar.dateInterval(of: .day, for: startOfCurrentDay)!
daysInMonth.append(dayRange)
startOfCurrentDay = dayRange.end.addingTimeInterval(1)
} while currentMonth.contains(startOfCurrentDay)
當我運行它時,我得到了這個:
[
2018-10-01 06:00:00 +0000 to 2018-10-02 06:00:00 +0000,
2018-10-02 06:00:00 +0000 to 2018-10-03 06:00:00 +0000,
2018-10-03 06:00:00 +0000 to 2018-10-04 06:00:00 +0000,
... snipped for brevity ...
2018-10-29 06:00:00 +0000 to 2018-10-30 06:00:00 +0000,
2018-10-30 06:00:00 +0000 to 2018-10-31 06:00:00 +0000,
2018-10-31 06:00:00 +0000 to 2018-11-01 06:00:00 +0000
]
讓我們回到上面創建的monthsOfYear
數組。 我們將使用它來計算每個月的周數:
for month in monthsOfYear {
let weekContainingStart = calendar.dateInterval(of: .weekOfMonth, for: month.start)!
print(weekContainingStart)
}
對於我的時區,這會打印:
2017-12-31 07:00:00 +0000 to 2018-01-07 07:00:00 +0000
2018-01-28 07:00:00 +0000 to 2018-02-04 07:00:00 +0000
2018-02-25 07:00:00 +0000 to 2018-03-04 07:00:00 +0000
2018-04-01 06:00:00 +0000 to 2018-04-08 06:00:00 +0000
2018-04-29 06:00:00 +0000 to 2018-05-06 06:00:00 +0000
2018-05-27 06:00:00 +0000 to 2018-06-03 06:00:00 +0000
2018-07-01 06:00:00 +0000 to 2018-07-08 06:00:00 +0000
2018-07-29 06:00:00 +0000 to 2018-08-05 06:00:00 +0000
2018-08-26 06:00:00 +0000 to 2018-09-02 06:00:00 +0000
2018-09-30 06:00:00 +0000 to 2018-10-07 06:00:00 +0000
2018-10-28 06:00:00 +0000 to 2018-11-04 06:00:00 +0000
2018-11-25 07:00:00 +0000 to 2018-12-02 07:00:00 +0000
您會在這里注意到的一件事是,這將星期日作為一周的第一天。 例如,在您的屏幕截圖中,您的一周包含 2 月 1 日,從 29 日開始。
該行為由Calendar
上的firstWeekday
屬性控制。 默認情況下,該值可能為1
(取決於您的語言環境),表示周從星期日開始。 如果您希望您的日歷在一周從Monday開始的地方進行計算,那么您需要將該屬性的值更改為2
:
var weeksStartOnMondayCalendar = calendar
weeksStartOnMondayCalendar.firstWeekday = 2
for month in monthsOfYear {
let weekContainingStart = weeksStartOnMondayCalendar.dateInterval(of: .weekOfMonth, for: month.start)!
print(weekContainingStart)
}
現在,當我運行該代碼時,我看到包含 2 月 1 日的那一周從 1 月 29 日“開始”:
2018-01-01 07:00:00 +0000 to 2018-01-08 07:00:00 +0000
2018-01-29 07:00:00 +0000 to 2018-02-05 07:00:00 +0000
2018-02-26 07:00:00 +0000 to 2018-03-05 07:00:00 +0000
2018-03-26 06:00:00 +0000 to 2018-04-02 06:00:00 +0000
2018-04-30 06:00:00 +0000 to 2018-05-07 06:00:00 +0000
2018-05-28 06:00:00 +0000 to 2018-06-04 06:00:00 +0000
2018-06-25 06:00:00 +0000 to 2018-07-02 06:00:00 +0000
2018-07-30 06:00:00 +0000 to 2018-08-06 06:00:00 +0000
2018-08-27 06:00:00 +0000 to 2018-09-03 06:00:00 +0000
2018-10-01 06:00:00 +0000 to 2018-10-08 06:00:00 +0000
2018-10-29 06:00:00 +0000 to 2018-11-05 07:00:00 +0000
2018-11-26 07:00:00 +0000 to 2018-12-03 07:00:00 +0000
有了所有這些,您就擁有了構建與 Calendar.app 所示相同的 UI 所需的所有工具。
作為額外的獎勵,無論您的日歷、時區和語言環境如何,我在此處發布的所有代碼都可以運行。 它可以為日本日歷、科普特日歷、希伯來日歷、ISO8601 等構建 UI。它可以在任何時區工作,也可以在任何語言環境中工作。
此外,擁有像這樣的所有DateInterval
值可能會使實現您的應用程序更容易,因為進行“范圍包含”檢查是您在制作日歷應用程序時想要進行的那種檢查。
唯一的另一件事是確保在將這些值呈現為String
時使用DateFormatter
。 請不要拉出單獨的日期組件。
如果您有后續問題,請將它們作為新問題發布,並在評論中@mention
我。
查看我幾年前寫的一些代碼來做類似的事情,它就像......
dayNumber
( CalendarUnit.weekday
)dayNumber - 1
那對我有用...
let calendar = Calendar.autoupdatingCurrent
let date = Date().description(with: Locale.current)
func firstDayOfMonth(month:Int, year:Int) -> Date {
var firstOfMonthComponents = DateComponents()
firstOfMonthComponents.calendar = calendar
firstOfMonthComponents.year = year
firstOfMonthComponents.month = month
firstOfMonthComponents.day = 01
return firstOfMonthComponents.date!
}
func firstMonday(month:Int, year:Int) -> Date {
let first = firstDayOfMonth(month: month, year: year)
let dayNumber = calendar.component(.weekday, from: first)
let daysToAdd = ((calendar.firstWeekday + 7) - dayNumber) % 7
return calendar.date(byAdding: .day, value: daysToAdd, to: first)!
}
(1...12).forEach {
print(firstMonday(month: $0, year: 2021).description(with: Locale.current))
}
我有第一個問題的答案:
第一個問題:我現在如何獲得包含該月第一天的一周中的第一個星期一? 例如,我如何獲得 2 月 29 日星期一或 4 月 26 日星期一? (請參閱 macOS 屏幕截圖)。 要到月底,我是否只需從開始的 42 天開始添加? 還是那太天真了?
let calendar = Calendar(identifier: .iso8601)
func firstOfMonth(month: Int, year: Int) -> Date {
var firstOfMonthComponents = DateComponents()
firstOfMonthComponents.calendar = calendar
firstOfMonthComponents.year = year
firstOfMonthComponents.month = month
firstOfMonthComponents.day = 01
return firstOfMonthComponents.date!
}
var aug01 = firstOfMonth(month: 08, year: 2018)
let dayOfWeek = calendar.component(.weekday, from: aug01)
if dayOfWeek <= 4 { //thursday or earlier
//take away dayOfWeek days from aug01
else {
//add 7-dayOfWeek days to aug01
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.