方便快捷地格式化时间。
通常我们对时间进行格式化,每次都会创建一个 DateFormatter,并且指定 dateFormat。代码如下。
1 2 3 4 5 6
| let date = Date()
let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd HH:mm"
let string = dateFormatter.string(from: date)
|
但是每次创建和设置 DateFormatter,显得十分繁琐,对此方法进行封装。代码如下。
1 2 3 4 5 6 7 8 9 10 11
| public extension Date { func string(format: String) -> String { let dateFormatter = DateFormatter() dateFormatter.dateFormat = format return dateFormatter.string(from: self) } }
let date = Date()
let string = date.string(format: "yyyy-MM-dd HH:mm")
|
大多数时候,dateFormat 是固定的,一直复制粘贴也容易出错,可以考虑提取成一个类型。代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| public extension DateFormatter { struct DateFormat: RawRepresentable, ExpressibleByStringLiteral { public var rawValue: String
public init?(rawValue: String) { self.rawValue = rawValue }
public init(stringLiteral value: String) { rawValue = value } } }
public extension Date { func string(dateFormat: DateFormatter.DateFormat) -> String { let dateFormatter = DateFormatter() dateFormatter.dateFormat = dateFormat.rawValue return dateFormatter.string(from: self) } }
public extension DateFormatter.DateFormat { static let MMddHHmm: DateFormatter.DateFormat = "MM-dd HH:mm" static let yyyyMMddHHmm: DateFormatter.DateFormat = "yyyy-MM-dd HH:mm" }
let date = Date()
let string1 = date.string(dateFormat: .MMddHHmm) let string2 = date.string(dateFormat: .yyyyMMddHHmm)
|
DateFormat 有一个类型为字符串的原始值,并且支持字符串字面量进行初始化。这时可以预先定义一些经常用到的常量,然后使用点语法直接访问。
外部调用已经强类型了,内部还是每次创建,可以考虑使用缓存。代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| private extension DateFormatter { static let dateFormatters = NSCache<NSString, DateFormatter>() }
private extension DateFormatter { static func computeCacheKey(dateFormat: DateFormatter.DateFormat) -> String { return [ "format", dateFormat.rawValue, ] .joined(separator: "|") } }
public extension DateFormatter { static func formatter(dateFormat: DateFormatter.DateFormat) -> DateFormatter { let cacheKey = computeCacheKey(dateFormat: dateFormat) as NSString
if let dateFormatter = dateFormatters.object(forKey: cacheKey) { return dateFormatter }
let dateFormatter = DateFormatter() dateFormatter.dateFormat = dateFormat.rawValue
DateFormatter.dateFormatters.setObject(dateFormatter, forKey: cacheKey)
return dateFormatter } }
|
为了修改方便,cacheKey 使用 joined(separator:) 生成,可以直接使用字符串插值。
时间格式化使用点语法调用具体的 DateFormat 已经足够,但不能明确表达此时格式化的业务需求。可以考虑定制一套 FormatStyle。
- short:只显示时间,不显示日期。
- long:显示月日和时间,非本年度显示年份。
- normal:按需显示。一分钟内显示「刚刚」,一小时内显示「xx 分钟前」,当天显示「xx 小时前」,昨天显示「昨天+时间」,前天显示「前天+时间」,三天及更早显示月日和时间。
代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| public extension Date { enum FormatStyle { case short case normal case long }
func string(for style: FormatStyle) -> String { switch style { case .short: return string(dateFormat: .HHmm) case .normal: let now = Date()
guard self <= now else { return string(dateFormat: .yyyyMMddHHmm) }
let originYear = Calendar.current.component(.year, from: self) let nowYear = Calendar.current.component(.year, from: now)
guard originYear == nowYear else { return string(dateFormat: .yyyyMMddHHmm) }
let originDays = Calendar.current.ordinality(of: .day, in: .year, for: self) let nowDays = Calendar.current.ordinality(of: .day, in: .year, for: now)
switch nowDays! - originDays! { case 3...: return string(dateFormat: .MMddHHmm) case 2: return "前天" + string(dateFormat: .HHmm) case 1: return "昨天" + string(dateFormat: .HHmm) default: let difference = Calendar.current.dateComponents([.hour, .minute], from: self, to: now)
guard difference.hour! < 1 else { return "\(difference.hour!)小时前" }
guard difference.minute! < 1 else { return "\(difference.minute!)分钟前" }
return "刚刚" } case .long: return Calendar.current.compare(self, to: Date(), toGranularity: .year) == .orderedSame ? string(dateFormat: .MMddHHmm) : string(dateFormat: .yyyyMMddHHmm) } } }
|
在 normal 分支中,首先判断是否为将来,是则返回具体时间。接着判断是否为本年度,不是则返回具体时间。然后调用 ordinality(of:in:for:) 获取该时间在一年中是第几天,这样可以处理跨月的情况。最后调用 dateComponents(_:from:to:) 获取相差的小时和分钟,判断后返回对应的描述。
在 normal 和 long 分支中,比较年份使用了不同的方法,但结果一样。
在 long 分支中,没有对将来进行判断,需注意。
Locale
一开始设计 API 时,除了 dateFormat,还引入了 locale,提供默认值 current。但后来发现 DateFormatter 的 locale 默认值就是 current,而且考虑到几乎没有用户经常改动语言地区设置而不重启 App,所以去除了 locale,只保留 dateFormat。
如果真的要设置 Locale,应该使用 autoupdatingCurrent,这个常量会追踪用户的修改。
Localized
除了直接修改 dateFormat,还有 setLocalizedDateFormatFromTemplate(_:) 可以使用,但两者的行为有些不同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| let df1 = DateFormatter() df1.locale = Locale(identifier: "zh_CN") df1.dateFormat = "yyyy-MM-dd HH:mm"
let df2 = DateFormatter() df2.locale = Locale(identifier: "zh_CN") df2.setLocalizedDateFormatFromTemplate("yyyy-MM-dd HH:mm")
let df3 = DateFormatter() df3.dateFormat = "yyyy-MM-dd HH:mm"
let df4 = DateFormatter() df4.setLocalizedDateFormatFromTemplate("yyyy-MM-dd HH:mm")
let date = Date()
for df in [df1, df2, df3, df4] { print(df.locale!, df.dateFormat!, df.string(from: date)) }
|
1 2 3 4
| zh_CN (fixed) yyyy-MM-dd HH:mm 2019-07-14 22:40 zh_CN (fixed) yyyy/MM/dd HH:mm 2019/07/14 22:40 en_US (current) yyyy-MM-dd HH:mm 2019-07-14 22:40 en_US (current) MM/dd/yyyy, HH:mm 07/14/2019, 22:40
|
直接修改 dateFormat,格式化文本不会受 locale 影响,所谓「所见即所得」。通过调用 setLocalizedDateFormatFromTemplate(_:),格式化文本会受 locale 影响,只使用其中的 Calendar.Component,其余的字符和顺序不会生效。
源码:Swift-Utils/DateFormatter