RelativeDateTimeFormatter: ماهیت، تاریخ‌های نسبی و Swift

نویسنده: IT Sectr منتشر شده: 2026-07-13 زمان مطالعه: 10 دقیقه

RelativeDateTimeFormatter کلاسی از Foundation در iOS و macOS است که تاریخ‌های مطلق را به عبارات نسبی خواناتر برای انسان تبدیل می‌کند: «۵ دقیقه پیش»، «دیروز»، «پس از ۳ روز». بر اساس مستندات توسعه‌دهندگان اپل، ۲۰۲۴، RelativeDateTimeFormatter به‌طور خودکار واحد مناسب (ثانیه، دقیقه، ساعت، روز) را انتخاب می‌کند و خروجی را به زبان لوکال فعلی دستگاه بومی‌سازی می‌کند. برخلاف محاسبه دستی اختلاف بین تاریخ‌ها از طریق Calendar، این کلاس ویژگی‌های زبانی هر زبان را در نظر می‌گیرد: در برخی زبان‌ها اعداد صرف می‌شوند، در برخی دیگر برای کلمه «دیروز» شکل ویژه‌ای استفاده می‌شود. این کلاس از iOS 13 و macOS 10.15 در دسترس است.

نکات کلیدی

  • RelativeDateTimeFormatter — کلاسی برای نمایش تاریخ‌های نسبی در iOS و macOS (iOS 13+)
  • خروجی بومی‌سازی‌شده — به‌طور خودکار عبارات را به زبان لوکال فعلی انتخاب می‌کند
  • سه نوع زمینه — past (قبل)، future (بعد)، present (اکنون) با عبارات متفاوت
  • انتخاب خودکار واحد — ثانیه، دقیقه، ساعت، روز، هفته، ماه، سال
  • پیکربندی سبک — numeric (پس از ۳ روز) یا abbreviated (پس از ۳ روز)

RelativeDateTimeFormatter چیست؟

RelativeDateTimeFormatter زیرکلاسی از Formatter در Foundation است که Date (یا اختلاف بر حسب ثانیه) را دریافت می‌کند و رشته بومی‌سازی‌شده‌ای با زمان نسبی برمی‌گرداند. برای مثال، برای تاریخی ۵ دقیقه زودتر از زمان فعلی، برای ru_RU «۵ دقیقه پیش» و برای en_US «5 minutes ago» برمی‌گرداند. این کلاس از سه زمینه زمانی پشتیبانی می‌کند: گذشته (past)، آینده (future) و حال (present).

منطق داخلی RelativeDateTimeFormatter از Calendar و Locale برای محاسبه اختلاف بین تاریخ‌ها و انتخاب شکل دستوری صحیح استفاده می‌کند. برای زبان فارسی، کلاس اعداد را به‌درستی صرف می‌کند: «۱ دقیقه پیش»، «۲ دقیقه پیش»، «۵ دقیقه پیش». برای انگلیسی بین «minute ago» و «minutes ago» انتخاب می‌کند. این قابلیت بر اساس داده‌های ICU (International Components for Unicode) است و به پیکربندی اضافی از سوی توسعه‌دهنده نیاز ندارد.

بر اساس Apple WWDC 2019، RelativeDateTimeFormatter بخشی از فریم‌ورک شد تا بومی‌سازی را ساده‌تر کند — پیش از ظهور آن، توسعه‌دهندگان مجبور بودند اختلاف تاریخ‌ها را به‌صورت دستی محاسبه و رشته‌های بومی‌سازی‌شده را از طریق String.localizedStringWithFormat وارد کنند. این امر به خطا در صرف (به‌ویژه برای زبان‌های اسلاوی و عربی) و انتخاب نادرست واحدهای اندازه‌گیری منجر می‌شد.

RelativeDateTimeFormatter چگونه «۵ دقیقه پیش» را نمایش می‌دهد؟

الگوریتم کار RelativeDateTimeFormatter از سه مرحله تشکیل شده است: محاسبه اختلاف بین تاریخ ارسال‌شده و لحظه فعلی، انتخاب واحد مناسب (بزرگ‌ترین واحدی که صفر نمی‌دهد) و قالب‌بندی با در نظر گرفتن لوکال. برای مثال، برای اختلاف ۳۷۲۰ ثانیه (۱ ساعت و ۲ دقیقه) واحد «ساعت» انتخاب می‌شود و نتیجه «۱ ساعت پیش» خواهد بود، نه «۶۲ دقیقه پیش».

واحدها بر اساس اصل «بزرگ‌ترین غیرصفر» انتخاب می‌شوند: اگر اختلاف بیشتر از ۸۶۴۰۰ ثانیه (۱ روز) باشد، روزها استفاده می‌شوند؛ اگر بیشتر از ۶۰۴۸۰۰ (۱ هفته) باشد — هفته‌ها و به همین ترتیب. این الگوریتم تضمین می‌کند نتیجه همیشه طبیعی خوانده شود: به‌جای «۵۱۸۴۰۰ ثانیه پیش» کاربر «۶ روز پیش» را می‌بیند. مرزهای دقیق واحدها توسط تقویم لوکال فعلی تعیین می‌شوند.

محدوده اختلافواحدمثال برای ru_RU
۰–۵۹ ثانیهSeconds۳۰ ثانیه پیش
۱–۵۹ دقیقهMinutes۵ دقیقه پیش
۱–۲۳ ساعتHours۳ ساعت پیش
۱–۶ روزDays۲ روز پیش
۷–۲۷ روزWeeks۱ هفته پیش
۲۸ روز–۱۱ ماهMonths۳ ماه پیش
۱۲+ ماهYears۱ سال پیش

زمینه قالب‌بندی پایان جمله را تعیین می‌کند. برای گذشته: «پیش» (فارسی)، «ago» (انگلیسی). برای آینده: «پس از ۳ روز» (فارسی)، «in 3 days» (انگلیسی). برای حال: «اکنون» (فارسی)، «now» (انگلیسی). زمینه از طریق متد localizeString(fromTimeInterval:) یا مستقیم از طریق string(from: Date) تنظیم می‌شود.

تنظیمات واحدها و سبک‌ها

RelativeDateTimeFormatter چندین تنظیم برای کنترل خروجی فراهم می‌کند: ویژگی unitsStyle سبک قالب‌بندی را تعیین می‌کند (numeric، abbreviated، full، spellOut)، و maximumUnitCount تعداد واحدهای نمایش‌داده‌شده را محدود می‌کند. برای مثال، با maximumUnitCount = 1 اختلاف ۱ ساعت و ۳۰ دقیقه به‌جای «۱ ساعت و ۳۰ دقیقه پیش» به‌صورت «۱ ساعت پیش» نمایش داده می‌شود.

سبک‌های قالب‌بندی

  • .numeric — مقدار عددی کامل: «۳ روز پیش»، «پس از ۲ هفته». برای رابط کاربری به‌صورت پیش‌فرض توصیه می‌شود
  • .abbreviated — شکل مخفف: «۳ روز پیش»، «پس از ۲ هفته». برای نمایش فشرده در جداول و فهرست‌ها
  • .full — شکل کلمه‌ای بدون رقم: «سه روز پیش». برای دسترس‌پذیری و رابط‌های صوتی
  • .spellOut — شکل حرفی با املای جایگزین: «three days ago». به‌ندرت و بیشتر برای کاربردهای ویژه استفاده می‌شود

محدودیت واحدها: به‌صورت پیش‌فرض RelativeDateTimeFormatter فقط یک واحد (بزرگ‌ترین) را نمایش می‌دهد. تنظیم maximumUnitCount = 2 واحد بعدی را برای توصیف دقیق‌تر فعال می‌کند: «۱ ساعت و ۳۰ دقیقه پیش». با این حال این کار می‌تواند رشته را برای پیام‌های کوتاه (پوش، اعلان) بیش از حد طولانی کند. برای رابط کاربری توصیه می‌شود maximumUnitCount = 1 باقی بماند.

swift
import Foundation

let formatter = RelativeDateTimeFormatter()

// پیکربندی سبک‌ها
formatter.unitsStyle = .numeric
formatter.maximumUnitCount = 1

// مثال‌هایی با تاریخ‌های مختلف
let fiveMinAgo = Date().addingTimeInterval(-300)
print("۵ دقیقه پیش: \(formatter.localizedString(for: fiveMinAgo, relativeTo: Date()))")

let twoDaysLater = Date().addingTimeInterval(172800)
print("۲ روز بعد: \(formatter.localizedString(for: twoDaysLater, relativeTo: Date()))")

// سبک مخفف
formatter.unitsStyle = .abbreviated
let oneWeekAgo = Date().addingTimeInterval(-604800)
print("مخفف: \(formatter.localizedString(for: oneWeekAgo, relativeTo: Date()))")

// سبک کامل (حروفی)
formatter.unitsStyle = .full
let threeHours = Date().addingTimeInterval(10800)
print("کامل: \(formatter.localizedString(for: threeHours, relativeTo: Date()))")

انتخاب سبک برای زمینه‌های مختلف: برای فید اخبار از .numeric با maximumUnitCount = 1 استفاده کنید — این استاندارد توییتر، اینستاگرام و فیسبوک است. برای دسترس‌پذیری (VoiceOver) از .full استفاده کنید — اعداد حروفی طبیعی‌تر خوانده می‌شوند. برای عناصر فشرده (گوشه اعلان، نوار وضعیت) برای صرفه‌جویی در فضا از .abbreviated استفاده کنید.

RelativeDateTimeFormatter در Swift: مثال‌ها

استفاده پایه RelativeDateTimeFormatter به ساخت یک نمونه، پیکربندی ویژگی‌ها و فراخوانی یکی از متدهای قالب‌بندی خلاصه می‌شود. متدهای اصلی: localizedString(for:relativeTo:) — برای یک جفت تاریخ، localizedString(fromTimeInterval:) — برای اختلاف بر حسب ثانیه، و string(for:) — برای Date با زمینه خودکار (گذشته/آینده).

swift
import Foundation

let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .numeric
formatter.maximumUnitCount = 1

// نمونه‌های رابط شبکه اجتماعی
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)")
}

// تاریخ‌های آینده
let reminderFormatter = RelativeDateTimeFormatter()
reminderFormatter.unitsStyle = .abbreviated
let inOneHour = Date().addingTimeInterval(3600)
let reminderText = reminderFormatter.localizedString(
    for: inOneHour,
    relativeTo: Date()
)
print("یادآوری: \(reminderText)")

مدیریت سناریوی «همین حالا» — RelativeDateTimeFormatter پشتیبانی داخلی از عبارت «همین حالا» برای بازه‌های بسیار کوچک ندارد. برای اختلاف کمتر از ۵ ثانیه «۰ ثانیه پیش» را برمی‌گرداند که در رابط کاربری زشت است. توصیه می‌شود فراخوانی formatter را در منطق شرطی بپیچید: اگر اختلاف کمتر از آستانه تعیین‌شده (مثلاً ۵ ثانیه) باشد — «همین حالا» را به‌صورت دستی نمایش دهید، در غیر این صورت تاریخ را به فرمتر پاس دهید.

swift
import Foundation

func relativeTimeString(from date: Date) -> String {
    let interval = Date().timeIntervalSince(date)

    // آستانه «همین حالا»
    if interval < 5 {
        return "just now"
    }

    // آستانه «امروز»
    if interval < 60 {
        return "just now"
    }

    let formatter = RelativeDateTimeFormatter()
    formatter.unitsStyle = .numeric
    formatter.maximumUnitCount = 1

    // نمایش بدون پسوند «پیش»
    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:) اختلاف بر حسب ثانیه را دریافت می‌کند و زمینه را به‌طور خودکار تعیین می‌کند (مقدار مثبت — آینده، منفی — گذشته). این روش زمانی راحت است که اختلاف از قبل مشخص باشد (مثلاً از سرور به‌صورت timestamp دریافت شده باشد). در این حالت نیازی به ساخت Date نیست — اختلاف مستقیم ارسال می‌شود.

بومی‌سازی تاریخ‌های نسبی

RelativeDateTimeFormatter به‌طور خودکار خروجی را بر اساس Locale.current بومی‌سازی می‌کند. برای تغییر زبان قالب‌بندی ویژگی locale را تنظیم کنید — برخلاف DateFormatter، برای RelativeDateTimeFormatter locale ثابت نیست و برای هر فراخوانی قابل تغییر است. این امکان را می‌دهد که تاریخ‌های نسبی به زبانی غیر از زبان رابط نمایش داده شوند (مثلاً محتوای به زبان اصلی).

پیچیدگی بومی‌سازی تاریخ‌های نسبی در ویژگی‌های دستوری زبان‌های مختلف است. زبان روسی به شکل‌های مختلف اعداد نیاز دارد: «۱ دقیقه»، «۲ دقیقه»، «۵ دقیقه». عربی — برای اعداد ۳ تا ۱۰ از شکل جمع و برای ۱۱+ از شکل‌های ویژه استفاده می‌کند. چینی — اصلاً صرف ندارد که کار را ساده می‌کند. RelativeDateTimeFormatter همه این موارد را از طریق قوانین ICU و بدون نیاز به کد اضافی پوشش می‌دهد.

swift
import Foundation

let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .numeric
formatter.maximumUnitCount = 1

let targetDate = Date().addingTimeInterval(-7200) // ۲ ساعت پیش

// لوکال‌های مختلف
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)")
}

// بررسی صرف روسی
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)) دقیقه: \(formatter.localizedString(for: date, relativeTo: Date()))")
}

نکته مهم: RelativeDateTimeFormatter هنگام محاسبه اختلاف برای تنظیمات .numeric از TimeZone صرف‌نظر می‌کند — از اختلاف مطلق بر حسب ثانیه استفاده می‌کند. اما برای سبک .full (با اعداد حروفی) و موارد ویژه (دیروز، امروز) TimeZone در نظر گرفته می‌شود. همیشه TimeZone را صریحاً تنظیم کنید، به‌ویژه اگر برنامه با تاریخ‌های سرور در UTC کار می‌کند.

خطاهای رایج هنگام قالب‌بندی

نادیده گرفتن TimeZone هنگام محاسبه تاریخ‌های نسبی — خطای رایج هنگام کار با تاریخ‌های سرور. اگر سرور Date را به‌صورت UTC ارسال کند و RelativeDateTimeFormatter از TimeZone.current استفاده کند، برای تاریخ‌های نزدیک به لحظه فعلی اختلاف ممکن است نادرست محاسبه شود. توصیه می‌شود برای داده‌های سرور همیشه formatter.timeZone = TimeZone(secondsFromGMT: 0) را تنظیم کنید.

انتخاب نادرست واحد برای بازه‌های کوتاه — RelativeDateTimeFormatter اختلاف را به بزرگ‌ترین واحد گرد می‌کند. برای ۲۵ ساعت نتیجه «۱ روز پیش» خواهد بود که ممکن است کاربر را گمراه کند. اگر دقت بالا لازم است (مثلاً برای تایمرهای شمارش معکوس)، به‌جای RelativeDateTimeFormatter از DateComponentsFormatter استفاده کنید — به شما امکان می‌دهد چند واحد را همزمان نمایش دهید.

عدم بررسی TimeInterval منفی — اگر تاریخ آینده به‌عنوان گذشته ارسال شود (مقدار منفی در string(fromTimeInterval:))، فرمتر ممکن است رشته نادرست برگرداند. همیشه علامت بازه را قبل از ارسال به فرمتر بررسی کنید، به‌ویژه هنگام کار با داده‌های سرور که منطقه زمانی می‌تواند محاسبه را مخدوش کند.

بر اساس Hacker News (2024)، یکی از پرطرفدارترین مشکلات RelativeDateTimeFormatter عدم پشتیبانی داخلی از «دیروز» و «امروز» برای زبان انگلیسی است. به‌جای «دیروز» فرمتر برای اختلاف ۹۰۰۰۰ ثانیه «۱ روز پیش» را برمی‌گرداند. برای زبان روسی چنین مشکلی نیست — «۱ روز پیش» طبیعی به نظر می‌رسد، اما برای رابط انگلیسی «yesterday» ترجیح داده می‌شود. این قابلیت پشتیبانی نمی‌شود و نیاز به بررسی دستی از طریق Calendar.isDateInToday/Yesterday دارد.

سوالات متداول

RelativeDateTimeFormatter چیست؟

RelativeDateTimeFormatter — کلاسی از Foundation برای نمایش تاریخ‌ها در قالب نسبی: «۵ دقیقه پیش»، «پس از ۲ روز». از iOS 13 و macOS 10.15 در دسترس است.

RelativeDateTimeFormatter واحدها را چگونه انتخاب می‌کند؟

بر اساس اصل بزرگ‌ترین واحد غیرصفر — ثانیه، دقیقه، ساعت، روز، هفته، ماه یا سال. برای مثال، برای اختلاف ۳۷۲۰ ثانیه (۱ ساعت و ۲ دقیقه) واحد «ساعت» انتخاب می‌شود، نه «دقیقه».

چگونه زبان خروجی را تغییر دهم؟

ویژگی locale را روی نمونه Locale موردنظر تنظیم کنید. به‌صورت پیش‌فرض Locale.current استفاده می‌شود. مثال: formatter.locale = Locale(identifier: "de_DE") برای زبان آلمانی.

تفاوت بین .numeric و .abbreviated چیست؟

.numeric — شکل کامل («۳ روز پیش»)، .abbreviated — مخفف («۳ روز پیش»). انتخاب بستگی به زمینه دارد: numeric برای رابط اصلی، abbreviated برای عناصر فشرده.

چگونه به‌جای «۰ ثانیه پیش» «همین حالا» را نمایش دهم؟

یک بررسی دستی برای بازه کمتر از ۵–۱۰ ثانیه اضافه کنید. RelativeDateTimeFormatter از «همین حالا» پشتیبانی نمی‌کند — برای بازه‌های کوچک «۰ ثانیه پیش» برمی‌گرداند. از منطق شرطی با آستانه استفاده کنید.

خلاصه

  • RelativeDateTimeFormatter — کلاس مناسب برای نمایش تاریخ‌های نسبی در iOS 13+
  • بومی‌سازی خودکار — صرف صحیح برای همه زبان‌های پشتیبانی‌شده از طریق ICU
  • سه سبک — .numeric (استاندارد)، .abbreviated (فشرده)، .full (حروفی)
  • انتخاب واحد — خودکار بر اساس اصل بزرگ‌ترین مقدار غیرصفر
  • تنظیم TimeZone — برای سازگاری هنگام کار با تاریخ‌های سرور ضروری است
  • آستانه «همین حالا» — به‌صورت داخلی پشتیبانی نمی‌شود؛ نیاز به بررسی دستی بازه دارد
  • بدون پشتیبانی از «دیروز» — برای زبان انگلیسی فرمتر از شکل yesterday استفاده نمی‌کند

ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد

IT Sectr از سال 2017 برنامه‌های iOS و Android را برای استارتاپ‌ها و کسب‌وکارها ایجاد می‌کند. ما به شما مشاوره می‌دهیم و بهترین راه‌حل را پیشنهاد خواهیم کرد.

بحث درباره پروژه

همچنین بخوانید