更新於 2022 年 09 月 11 日
# iOS15+ 的日期格式化方法
iOS15 增加了新的格式化 API,方便我們描述如何在不同類型之間轉換。同時,也提供了許多內建的 FormatStyle,其中「Date.FormatStyle」,就是指從 Date 轉換成 String 。
- 轉換具體時間、相對時間或是一段時間。
- 預設的轉換結果會根據使用者的 Locale 設定而不同。
(以下示範都用西曆 + 台灣中文)
💡 如果你在Playground 測試,可以加上這個 extension 來調整預設的語系和日曆。
extension NSLocale {
@objc
static let currentLocale = NSLocale(localeIdentifier: "語系和日曆 ID")
}
ID 範例
台灣中文:"zh_TW"
中文和台灣日曆:"zh_TW@calendar=roc"
日文和日本日曆:"ja_JP@calendar=japanese"
# 具體時間
預設風格
- 不加參數,直接根據系統印出預設的日期格式。
Date.now.formatted() // 2022/9/11 下午8:38
- 用 date 和 time 參數表示想要呈現的「長度」。
date.formatted(date: .complete, time: .complete) // 2022年9月11日 星期日 GMT+8 下午8:38:42 date.formatted(date: .long, time: .standard) // 2022年9月11日 下午8:38:42 date.formatted(date: .abbreviated, time: .shortened) // 2022年9月11日 下午8:38 date.formatted(date: .numeric, time: .omitted) // 2022/9/11
自訂風格
上面我們在 date 和 time 的參數中放的其實就是預設提供的幾種 Date.FormatStyle
選項,我們也可以透過以下兩種類型開始建立。
Date.FormatStyle
:根據 Locale 轉換成不同的語言和日期。Date.ISO8601FormatStyle
:固定 ISO8601 格式。
上面兩種都可以直接在 formatted 裡面快速建立:
date.formatted(.dateTime)
// 2022年9月11日 下午8:38
date.formatted(.iso8601)
// 2022-09-11T12:38:42Z"
現在跟上面預設的結果一樣,我們可以接著用類型裡的方法描述要顯示「哪些日期和時間單位,以及顯示方式」,以下簡單示範幾種。
- 順序不會產生影響,一律套用 Locale 慣用排序。
date.formatted(.iso8601.year().month().day())
// 2022-09-11
date.formatted(.dateTime.month().day().hour().minute().weekday(.wide))
// 9月11日 星期日 下午8:38
date.formatted(.dateTime.hour().minute().second().timeZone(.genericLocation))
// 台灣時間 下午8:38:42
date.formatted(.dateTime.year().quarter())
// 2022年3季
date.formatted(.dateTime.year().week())
// 2022年 (週: 38)
⚠️ 要注意的是省略「amPM」並不會自動轉成 24 小時制,12/24 小時制還是根據系統設定。
以下示範一個晚上 10 點的顯示結果。
let pmTime = Calendar.current.date(bySetting: .hour, value: 22, of: date)!
pmTime.formatted(.dateTime.hour().minute())
// 下午10:00
pmTime.formatted(.dateTime.hour(.defaultDigits(amPM: .omitted)).minute())
// 10:00
// 手動改成日本 Locale。
let japanLocale = Locale(identifier: "ja_JP")
pmTime.formatted(.dateTime.hour().minute().locale(japanLocale))
// 22:00 << 預設就已經是 24 小時制。
pmTime.formatted(.dateTime.hour(.defaultDigits(amPM: .omitted)).minute().locale(japanLocale))
// 22:00
但是這個改法還是可能受到一些其他設定影響,所以目前想要顯示 24 小時制的話還是用舊方法最安全。
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm"
formatter.string(from: pmTime)
// 22:00
轉成方便的 Extension
這個 Format API 使用上很直覺,但是直接加在 date 後面很容易讓程式碼顯得很雜亂,建議把自己常用的 FormatStyle 加到 extension 中使用。
以下是我自己用的 Extension,因為我已經太習慣格式化代碼了,所以直接用代碼命名。
extension FormatStyle where Self == Date.FormatStyle {
static var MMMd: Self { dateTime.MMMd }
static var hhmm: Self { dateTime.hhmm }
var MMMd: Self { self.month(.abbreviated).day() }
var E: Self { self.weekday(.abbreviated) }
var hhmm: Self { self.hour(.conversationalTwoDigits(amPM: .wide)).minute(.twoDigits) }
var gmt: Self { self.timeZone(.localizedGMT(.long)) }
var gmtLocalized: Self { self.timeZone(.specificName(.long)) }
}
// 顯示日期
date.formatted(.MMMd) // 9月11日
date.formatted(.MMMd.E) // 9月11日 週日
date.formatted(.MMMd.E.year()) // 2022年9月11日 週日
// 顯示時間
date.formatted(.hhmm) // 下午08:38
date.formatted(.hhmm.gmt) // GMT+08:00 下午08:38
date.formatted(.hhmm.gmtLocalized) // 台北標準時間 下午08:38
// 顯示日期 + 時間
date.formatted(.MMMd.hhmm) // 9月11日 下午08:38
date.formatted(.MMMd.E.hhmm) // 9月11日 週日 下午08:38
// 如果是在 SwiftUI,當你的 Text 只有要顯示時間資訊時,用 format 參數會讓你的程式碼更清晰。
Text(date, format: .hhmm)
Text(date, format: .MMMd.hhmm.second(.twoDigits))
設定時區和語系
通常用系統設定的時區和語系會比較好,但也可能有特別情況你想固定。
可以在啟動 Date.FormatStyle
時加上這些參數。
注意這個 FormatStyle 中的「屬性」timeZone 是使用的時區,而「方法」 timeZone 是描述如何顯示時區資訊。
let engFormatter = Date.FormatStyle.init(locale: .init(identifier: "en_GB"),
timeZone: .init(abbreviation: "BST")!)
date.formatted(.MMMd.HHmm.gmtLocalized)
// 9月11日 台北標準時間 下午08:38
date.formatted(engFormatter.MMMd.HHmm.gmtLocalized)
// 11 Sep, 01:38 pm British Summer Time
轉回 Date
除了轉成 String,這兩種類型也有遵循 ParseStrategy
,所以也可以從 String 轉回 Date,不過要建立 Date 一定要有完整的資訊,搭配 .datetime / .iso8601
提供預設值。
try! Date("2022-09-11T12:38:42Z", strategy: .iso8601)
// 2022年9月11日 下午8:38
try! Date("2022-09-11", strategy: .iso8601.year().month().day())
// 2022年9月11日 上午8:00(預設 UTC+0 0 點時間)
try! Date("9月11日", strategy: .dateTime.MMMd)
// 2022年9月11日 上午5:47 (沒有提供的年和時間會是你現在的時間)
# 相對時間
使用 Date.RelativeFormatStyle
時間轉換成和現在相對的時間,可以用 .relative
來快速啟動。
let afterTomorrow = Calendar.current.date(byAdding: .day, value: 2, to: Date())!
afterTomorrow.formatted(.relative(presentation: .named, unitsStyle: .wide))
// 後天
afterTomorrow.formatted(.relative(presentation: .numeric, unitsStyle: .wide))
// 2天後
afterTomorrow.formatted(.relative(presentation: .numeric, unitsStyle: .spellOut))
// 二天後
不過這個寫法固定就是和現在比較,如果要設定相對於哪個時間,一樣得用舊方法。
let relativeFormatter = RelativeDateTimeFormatter()
relativeFormatter.locale = .current
relativeFormatter.dateTimeStyle = .numeric
relativeFormatter.unitsStyle = .full
let xMas = try! Date("2022/12/25", strategy: .dateTime.year().month().day())
let timeToXMas = relativeFormatter.localizedString(for: xMas, relativeTo: .now)
print("還有\(timeToXMas)到聖誕節🎄")
// 還有3個月後到聖誕節🎄
let lastXMas = try! Date("2021/12/25", strategy: .dateTime.year().month().day())
let timeToLastXMas = relativeFormatter.localizedString(for: lastXMas, relativeTo: .now)
print("🤶上次聖誕節是 \(timeToLastXMas)")
// 上次聖誕節是 8個月前
# 一段時間
最後,也可以使用 Date.IntervalFormatStyle
搭配 Range<Date>
來顯示一段時間。同樣也有快速啟動的 .interval
。
(date..<afterTomorrow).formatted(.interval)
// 2022/9/11 下午8:38至2022/9/14 上午6:56
(date..<afterTomorrow).formatted(.interval.day())
// 11日至14日
(date..<afterTomorrow).formatted(.interval.weekday())
// 週日至週三
(date..<afterTomorrow).formatted(.interval.day().hour())
// 11日 下午8時至14日 上午6時
(date..<afterTomorrow).formatted(.interval.month().day())
// 9月11日至14日
(date..<afterTomorrow).formatted(.interval.month().day().year())
// 2022年9月11日至14日
以上介紹的這些方法,相信已經能滿足大部分的需求,不過如果你還有特別的需求,你也可以建立自己的 FormatStyle 類型來描述如何轉換。