しおメモ

雑多な技術系ブログです。ニッチな内容が多いです。

よくある実装でDateComponentsFormatterやRelativeDateTimeFormatterを使う

DateFormatterに関しては古くからあり、おおよそのプロジェクトで使われているイメージですが、DateComponentsFormatterRelativeDateTimeFormatter(iOS13+なので最近)については体感としてあまり使われておらず、特に歴史のあるプロジェクトでは独自のformatterで実装しているケースが多いので、書いてみました。

全てのケースで使えるわけではないですが、特に多言語対応しているアプリでは、大きく実装量が減って楽にはなるので、選択肢の一つとしてあり得ると思います。

標準のFormatterを使う意義

独自のformatterを利用するのに対して、標準のformatterを使うことの一番のメリットは、言語ごとに追加で実装することなく、他の言語での表記方法に対応できるところです。

これについては、昨年のWWDCの発表が詳しいですが、こちらではdate関連以外のformatterについても紹介されています。

developer.apple.com

独自のformatterは、一度書いてしまえば動きが変わることが少ない部分ではあるので、運用面では問題ないと思います。
しかし、なんだかんだテストを書いたり、分岐を多く書かないといけないケースもあるので、使える際には標準のものを使いたいところです。 今回のformatterで言えば、TimeIntervalも引数に取れるので、60で割る処理を書かなくて済むケースもあり得ます。

デメリットとしては、標準で用意された表記方法をしたい場合に小回りが利かないという点があます。 例えばあるケースだけ違う表記をしたいという場合は、大体個別に処理を書く必要があります。

使用例

実際にありそうなケースで、DateComponentsFormatterRelativeDateTimeFormatterを使ってみます。

Case 1: 残り時間のカウントダウン

ECアプリのキャンペーンなどでよくある"残り3日"とか"残り8時間"のような表記です。 こちらはDateComponentsFormatterのみで実装できます。

let formatter = DateComponentsFormatter()
formatter.unitsStyle = .full
formatter.allowedUnits = [.day, .hour, .minute]
formatter.maximumUnitCount = 1
formatter.includesTimeRemainingPhrase = true

unitsStyleによって単位の表記の方法が変わります。(hours, hrs, hなど)

https://developer.apple.com/documentation/foundation/datecomponentsformatter/unitsstyle

allowedUnitsで指定した日, 時, 分のみが利用され、maximumUnitCountが1なので一番大きな単位のみが表示されます。 includesTimeRemainingPhraseをつけることで"残り"の部分も各言語に合わせてつけてくれます。

print(formatter.string(from: 36)!) // 残り0分, 0 minutes remaining
print(formatter.string(from: 192)!) // 残り3分, 3 minutes remaining
print(formatter.string(from: 10922)!) // 残り3時間, 3 hours remaining
print(formatter.string(from: 90000)!) // 残り1日, 1 day remaining

残り時間によって表記が変わるなど、こだわったviewでなければ、これはそのまま使えそうです。

Case 2: 再生時間の表示

動画アプリや音楽アプリの再生時間などである1:52のような表記です。

言語によらない表記のためメリットが薄いのと、後述の問題があるので、独自実装でも良いケースではありますが、動画や音楽の再生時間や残り再生時間を表示する際もDateComponentsFormatterが利用できます。

1:52のような形式で表示する際はpositionalを利用します。

let formatter = DateComponentsFormatter()
formatter.unitsStyle = .positional // defaultでpositional
formatter.allowedUnits = [.minute, .second]

01:52のようにゼロ埋めをする場合は、zeroFormattingBehaviorを利用します。

formatter.zeroFormattingBehavior = .pad
print(formatter.string(from: 36)!) // 00:36
print(formatter.string(from: 192)!) // 03:12
print(formatter.string(from: 10922)!) // 182:02

zeroFormattingBehaviorを設定した際は負数を入れた際に、想定した挙動にならないケースがあるので、YouTubeのような-01:52のような表示をしたい場合は注意が必要です。

print(formatter.string(from: -36)!) // 00:36 (-00:36にはしてくれない)
print(formatter.string(from: -192)!) // -03:12

また、0埋めの0の個数を変えられなかったり、"時間は省いて良いが、分は必ず表示させる"ような挙動を実現できなかったりするので、その場合は素直に独自のformatterを使ったり、複数のformatterを組み合わせるのが良さそうです。

Case 3: 投稿時間の相対表記

Instagramなどの投稿型のアプリでよく見る"n分前"や"n日前"といった表記は、RelativeDateTimeFormatterで実現できます。

let formatter = RelativeDateTimeFormatter()
formatter.dateTimeStyle = .named
formatter.unitsStyle = .full

dateTimeStylenamedにすると、"1 day ago", "1 week ago"の代わりに"yesterday", "last week"などの表記をしてくれます。 正負で未来か過去かを判断してくれます。

print(formatter.localizedString(fromTimeInterval: -3)) // 3 秒前, 3 seconds ago
print(formatter.localizedString(fromTimeInterval: -192)) // 3 分前, 
3 minutes ago
print(formatter.localizedString(fromTimeInterval: -10802)) // 3 時間前, 3 hours ago
print(formatter.localizedString(fromTimeInterval: -90000)) // 昨日, yesterday

古すぎる場合に日時をそのまま表示をしたいケースでは、別途DateFormatterと組み合わせることになります。また、DateComponentsFormatterと違い、allowedUnitsのようなプロパティがないので、"nヶ月前"だけ"n日前"に直すみたいなことができません。

また、文中で使う場合などは、formattingContextを設定します。

formatter.formattingContext = .beginningOfSentence

この場合Yesterdayのように先頭を大文字にして返してくれます。

Twitterのように"n分"、"n日"の表記であれば、単に先ほどのDateComponentsFormatterを使うケースも考えられます。
先ほどのカウントダウンとほぼ同じで、

let formatter = DateComponentsFormatter()
formatter.unitsStyle = .abbreviated
formatter.allowedUnits = [.day, .hour, .minute, .second]
formatter.maximumUnitCount = 1

のような設定にすれば、

print(formatter.string(from: 36)!) // 36秒, 36s
print(formatter.string(from: 192)!) // 3分, 3m
print(formatter.string(from: 10922)!) // 3時間, 3h 
print(formatter.string(from: 90000)!) // 1日, 1d
print(formatter.string(from: 7257600)!) // 84日, 84d

このような結果が得られます。

RelativeDateTimeFormatterは未来の日時についても"tomorrow", "in n days"などの表記をしてくれるので、そこまでformatterに任せるかで使い分けができそうです。

余談

最近Public PreviewになったVSCode for the Webを使って書きましたが、markdownのプレビューも使えて、デスクトップ版と変わらない操作感で便利でした。