RelativeDateTimeFormatterは、iOSとmacOSのFoundationクラスで、絶対日付を人間が読める相対表現に変換します:「5分前」、「昨日」、「3日後」。Apple Developer Documentation, 2024によると、RelativeDateTimeFormatterは適切な単位(秒、分、時間、日)を自動的に選択し、デバイスの現在のロケールの言語で出力をローカライズします。Calendarを介して日付間の差を手動計算するのとは異なり、このクラスは各言語の言語学的特徴を考慮します:一部の言語では数詞が活用され、他の言語では「昨日」という単語に特別な形式が使用されます。このクラスはiOS 13およびmacOS 10.15以降で利用可能です。
重要なポイント
RelativeDateTimeFormatterは、FoundationのFormatterのサブクラスで、Date(または秒単位の差)を受け取り、相対時間を含むローカライズされた文字列を返します。例えば、現在より5分前の日付の場合、「5分前」を返します。このクラスは3つの時間コンテキスト(過去、未来、現在)をサポートしています。
RelativeDateTimeFormatterの内部ロジックは、CalendarとLocaleを使用して日付間の差を計算し、正しい文法形式を選択します。日本語の場合、「1分前」と「5分前」を適切に選びます。この機能はICU(International Components for Unicode)データに基づいており、開発者による追加設定は不要です。
Apple WWDC 2019によると、RelativeDateTimeFormatterはローカライズを簡素化するためにフレームワークの一部となりました。導入前は、開発者は手動で日付の差を計算し、String.localizedStringWithFormatを介してローカライズされた文字列を置き換える必要がありました。これにより、活用エラー(特にスラブ語やアラビア語)や測定単位の誤った選択が発生していました。
アルゴリズムは3つのステップで構成されます:渡された日付と現在時刻の差の計算、適切な単位の選択(ゼロにならない最大のもの)、ロケールに従った書式設定。例えば、3720秒(1時間2分)の差の場合、「時間」単位が選択され、結果は「1時間前」となり、「62分前」とはなりません。
単位は「最大非ゼロ」の原則に従って選択されます:差が86400秒(1日)を超える場合は日、604800(1週間)を超える場合は週が使用されます。このアルゴリズムにより、結果が常に自然に読めることが保証されます:「518400秒前」ではなく、ユーザーは「6日前」を目にします。単位の正確な境界は現在のロケールのカレンダーによって決定されます。
| 差の範囲 | 単位 | ja_JPの例 |
|---|---|---|
| 0〜59秒 | Seconds | 30秒前 |
| 1〜59分 | Minutes | 5分前 |
| 1〜23時間 | Hours | 3時間前 |
| 1〜6日 | Days | 2日前 |
| 7〜27日 | Weeks | 1週間前 |
| 28日〜11ヶ月 | Months | 3ヶ月前 |
| 12ヶ月以上 | Years | 1年前 |
書式設定コンテキストはフレーズの末尾を決定します。過去:「前」(日本語)。未来:「3日後」(日本語)。現在:「今」(日本語)。コンテキストはlocalizeString(fromTimeInterval:)メソッドまたはstring(from: Date)を介して直接設定されます。
RelativeDateTimeFormatterは出力を制御するためのいくつかの設定を提供します:unitsStyleプロパティは書式設定スタイル(numeric、abbreviated、full、spellOut)を決定し、maximumUnitCountは表示される単位の数を制限します。例えば、maximumUnitCount = 1の場合、1時間30分の差は「1時間30分前」ではなく「1時間前」と表示されます。
単位の制限:デフォルトでは、RelativeDateTimeFormatterは1つの(最大の)単位のみを表示します。maximumUnitCount = 2を設定すると、より正確な説明のために次の単位が含まれます:「1時間30分前」。ただし、これにより短いメッセージ(プッシュ通知、アラート)では文字列が過度に長くなる可能性があります。UIでは、maximumUnitCount = 1を維持することをお勧めします。
import Foundation
let formatter = RelativeDateTimeFormatter()
// Configure styles
formatter.unitsStyle = .numeric
formatter.maximumUnitCount = 1
// Examples with different dates
let fiveMinAgo = Date().addingTimeInterval(-300)
print("5 min ago: \(formatter.localizedString(for: fiveMinAgo, relativeTo: Date()))")
let twoDaysLater = Date().addingTimeInterval(172800)
print("2 days later: \(formatter.localizedString(for: twoDaysLater, relativeTo: Date()))")
// Abbreviated style
formatter.unitsStyle = .abbreviated
let oneWeekAgo = Date().addingTimeInterval(-604800)
print("Abbreviated: \(formatter.localizedString(for: oneWeekAgo, relativeTo: Date()))")
// Full style (spelled out)
formatter.unitsStyle = .full
let threeHours = Date().addingTimeInterval(10800)
print("Full: \(formatter.localizedString(for: threeHours, relativeTo: Date()))")
異なるコンテキストでのスタイルの選択:ニュースフィードには、maximumUnitCount = 1で.numericを使用します — これはTwitter、Instagram、Facebookの標準です。アクセシビリティ(VoiceOver)には、.fullを使用します — スペルアウトされた数字はより自然に読み上げられます。コンパクトな要素(通知バッジ、ステータスバー)には、スペースを節約するために.abbreviatedを使用します。
基本的な使用法は、インスタンスの作成、プロパティの設定、書式設定メソッドの呼び出しに集約されます。主なメソッドは:localizedString(for:relativeTo:) — 日付のペア用、localizedString(fromTimeInterval:) — 秒単位の差用、string(for:) — 自動コンテキスト(過去/未来)付きのDate用です。
import Foundation
let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .numeric
formatter.maximumUnitCount = 1
// Social network UI examples
let postDates: [(title: String, date: Date)] = [
("Just now", Date().addingTimeInterval(-30)),
("5 min ago", Date().addingTimeInterval(-300)),
("Yesterday", Date().addingTimeInterval(-90000)),
("Last week", Date().addingTimeInterval(-700000)),
("Last year", Date().addingTimeInterval(-32000000))
]
for (title, postDate) in postDates {
let relative = formatter.localizedString(
for: postDate,
relativeTo: Date()
)
print("\(title): \(relative)")
}
// Future dates
let reminderFormatter = RelativeDateTimeFormatter()
reminderFormatter.unitsStyle = .abbreviated
let inOneHour = Date().addingTimeInterval(3600)
let reminderText = reminderFormatter.localizedString(
for: inOneHour,
relativeTo: Date()
)
print("Reminder: \(reminderText)")
「たった今」シナリオの処理 — RelativeDateTimeFormatterには、非常に短い間隔に対する「たった今」という表現の組み込みサポートがありません。5秒未満の差の場合、「0秒前」を返し、UIで見栄えが悪くなります。フォーマッタの呼び出しを条件付きロジックでラップすることをお勧めします:差が設定されたしきい値(例:5秒)未満の場合は手動で「たった今」を表示し、それ以外の場合は日付をフォーマッタに渡します。
import Foundation
func relativeTimeString(from date: Date) -> String {
let interval = Date().timeIntervalSince(date)
// "Just now" threshold
if interval < 5 {
return "just now"
}
// "Today" threshold
if interval < 60 {
return "just now"
}
let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .numeric
formatter.maximumUnitCount = 1
// Display without "ago" suffix
return formatter.localizedString(
for: date,
relativeTo: Date()
)
}
print(relativeTimeString(from: Date().addingTimeInterval(-3)))
print(relativeTimeString(from: Date().addingTimeInterval(-120)))
print(relativeTimeString(from: Date().addingTimeInterval(-3600)))
string(fromTimeInterval:)メソッドは秒単位の差を受け入れ、自動的にコンテキストを決定します(正の値 — 未来、負の値 — 過去)。これは差が既にわかっている場合(例:サーバーからUnixタイムスタンプとして受信)に便利です。この場合、Dateを作成する必要はなく、差が直接渡されます。
RelativeDateTimeFormatterはLocale.currentに基づいて自動的に出力をローカライズします。書式設定言語を変更するには、localeプロパティを設定します — DateFormatterとは異なり、RelativeDateTimeFormatterではロケールは固定されておらず、呼び出しごとに変更できます。これにより、インターフェース言語とは異なる言語で相対日付を表示できます(例:元の言語のコンテンツ)。
相対日付のローカライズの複雑さは、異なる言語の文法的特徴にあります。日本語では「1分前」、「5分前」のように数詞の後に適切な助数詞が付きます。アラビア語は3〜10の数字に複数形、11以上に特別な形式を使用します。中国語には活用がなく、タスクが簡素化されます。RelativeDateTimeFormatterはICUルールを通じてこれらすべてのケースをカバーし、追加のコードは不要です。
import Foundation
let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .numeric
formatter.maximumUnitCount = 1
let targetDate = Date().addingTimeInterval(-7200) // 2 hours ago
// Different locales
let locales: [String] = ["ru_RU", "en_US", "de_DE", "fr_FR", "ja_JP", "ar_SA"]
for identifier in locales {
formatter.locale = Locale(identifier: identifier)
let result = formatter.localizedString(
for: targetDate,
relativeTo: Date()
)
print("\(identifier): \(result)")
}
// Check Japanese pluralization
formatter.locale = Locale(identifier: "ru_RU")
let intervals: [TimeInterval] = [-60, -120, -180, -300]
for interval in intervals {
let date = Date().addingTimeInterval(interval)
print("\(-Int(interval / 60)) min: \(formatter.localizedString(for: date, relativeTo: Date()))")
}
重要なニュアンス:RelativeDateTimeFormatterは、.numeric設定の差の計算時にTimeZoneを無視します — 秒単位の絶対差を使用します。ただし、.fullスタイル(スペルアウトされた数字)と特別なケース(昨日、今日)では、TimeZoneが考慮されます。一貫性のために常にTimeZoneを明示的に設定してください。特にアプリケーションがUTCのサーバー日付を扱う場合は重要です。
相対日付計算時のTimeZoneの無視 — サーバー日付を扱う際の一般的な間違いです。サーバーがUTCでDateを送信し、RelativeDateTimeFormatterがTimeZone.currentを使用する場合、現在時刻に近い日付の差が正しく計算されない可能性があります。サーバーデータには常にformatter.timeZone = TimeZone(secondsFromGMT: 0)を設定することをお勧めします。
短い間隔での誤った単位選択 — RelativeDateTimeFormatterは差を最大単位に丸めます。25時間の場合、結果は「1日前」となり、ユーザーを誤解させる可能性があります。高い精度が必要な場合(例:カウントダウンタイマー)、RelativeDateTimeFormatterの代わりにDateComponentsFormatterを使用してください — 複数の単位を同時に表示できます。
負のTimeIntervalのチェック不足 — 未来の日付が過去として渡された場合(string(fromTimeInterval:)で負の値)、フォーマッタは誤った文字列を返す可能性があります。フォーマッタに渡す前に常に間隔の符号をチェックしてください。特にタイムゾーンが計算を歪める可能性のあるサーバーデータを扱う場合は重要です。
Hacker News(2024)によると、RelativeDateTimeFormatterの最も議論されている問題の1つは、英語での「昨日」と「今日」の組み込みサポートの欠如です。「昨日」の代わりに、90000秒の差のフォーマッタは「1日前」を返します。日本語にはそのような問題はなく、「1日前」は自然に聞こえますが、英語のUIでは「yesterday」が好ましいです。この機能はサポートされておらず、Calendar.isDateInToday/Yesterdayによる手動チェックが必要です。
よくある質問
RelativeDateTimeFormatterは、日付を相対形式で表示するためのFoundationクラスです:「5分前」、「2日後」。iOS 13およびmacOS 10.15以降で利用可能です。
最大非ゼロ単位の原則による — 秒、分、時間、日、週、月、年。例えば、3720秒(1時間2分)の差の場合、「時間」単位が選択され、「分」ではありません。
localeプロパティを設定します目的のLocaleインスタンスに。デフォルトではLocale.currentが使用されます。例:ドイツ語の場合、formatter.locale = Locale(identifier: "de_DE")。
.numeric — 完全形式(「3日前」)、.abbreviated — 短縮形式(「3日前」)。選択はコンテキストに依存します:メインUIにはnumeric、コンパクト要素にはabbreviated。
手動チェックを追加します5〜10秒未満の間隔に対して。RelativeDateTimeFormatterは「たった今」をサポートしていません — 小さな間隔では「0秒前」を返します。しきい値付きの条件付きロジックを使用してください。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。