RelativeDateTimeFormatter هي كلاس في Foundation في iOS وmacOS تحول التواريخ المطلقة إلى صيغ نسبية قابلة للقراءة: «قبل 5 دقائق»، «أمس»، «بعد 3 أيام». وفقًا Apple Developer Documentation, 2024، يقوم RelativeDateTimeFormatter تلقائيًا باختيار الوحدة المناسبة (ثوانٍ، دقائق، ساعات، أيام) ويوطّن المخرجات بلغة الإعدادات المحلية الحالية للجهاز. على عكس الحساب اليدوي للفرق بين التواريخ عبر Calendar، تراعي هذه الكلاس الخصائص اللغوية لكل لغة: بالنسبة لبعض اللغات تُصرف الأرقام، وبالنسبة لأخرى تُستخدم صيغة خاصة لكلمة «أمس». الكلاس متاح بدءًا من iOS 13 وmacOS 10.15.
الخلاصة
RelativeDateTimeFormatter هي فئة فرعية من Formatter في Foundation تأخذ Date (أو فرقًا بالثواني) وتعيد سلسلة محلية مع الوقت النسبي. على سبيل المثال، لتاريخ قبل 5 دقائق من التاريخ الحالي، تعيد «قبل 5 دقائق». تدعم الكلاس ثلاثة سياقات زمنية: الماضي والمستقبل والحاضر.
يستخدم المنطق الداخلي لـ RelativeDateTimeFormatter كلاً من Calendar وLocale لحساب الفرق بين التواريخ واختيار الصيغة النحوية الصحيحة. بالنسبة للعربية، يختار بين «قبل دقيقة» و«قبل دقيقتين» و«قبل 5 دقائق». تستند هذه الوظيفة إلى بيانات ICU (المكونات الدولية لـ Unicode) ولا تتطلب أي تكوين إضافي من المطور.
وفقًا Apple WWDC 2019، أصبح RelativeDateTimeFormatter جزءًا من الإطار لتبسيط التوطين — قبل ظهوره، كان المطورون مضطرين لحساب فرق التواريخ يدويًا واستبدال السلاسل المحلية عبر String.localizedStringWithFormat. مما أدى إلى أخطاء في التصريف (خاصة للغات السلافية والعربية) واختيار غير صحيح لوحدات القياس.
الخوارزمية لـ RelativeDateTimeFormatter تتكون من ثلاث خطوات: حساب الفرق بين التاريخ المُمرر واللحظة الحالية، اختيار الوحدة المناسبة (الأكبر التي لا تعطي صفرًا)، والتنسيق حسب الإعدادات المحلية. على سبيل المثال، لفرق 3720 ثانية (ساعة ودقيقتان)، يتم اختيار وحدة «ساعة» وتكون النتيجة «قبل ساعة واحدة»، وليس «قبل 62 دقيقة».
تُختار الوحدات على مبدأ «الأكبر غير الصفري»: إذا كان الفرق أكبر من 86400 ثانية (يوم واحد)، تُستخدم الأيام؛ إذا كان أكبر من 604800 (أسبوع واحد) — الأسابيع، وهكذا. تضمن هذه الخوارزمية أن تكون النتيجة دائمًا طبيعية القراءة: بدلاً من «قبل 518400 ثانية»، يرى المستخدم «قبل 6 أيام». يتم تحديد الحدود الدقيقة للوحدات بواسطة تقويم الإعدادات المحلية الحالية.
| نطاق الفرق | الوحدة | مثال لـ ar_SA |
|---|---|---|
| 0–59 ثانية | Seconds | قبل 30 ثانية |
| 1–59 دقيقة | Minutes | قبل 5 دقائق |
| 1–23 ساعة | Hours | قبل 3 ساعات |
| 1–6 أيام | Days | قبل يومين |
| 7–27 يومًا | Weeks | قبل أسبوع |
| 28 يومًا–11 شهرًا | Months | قبل 3 أشهر |
| 12+ شهرًا | Years | قبل سنة |
سياق التنسيق يحدد نهاية العبارة. للماضي: «قبل» (عربية). للمستقبل: «بعد 3 أيام» (عربية). للحاضر: «الآن» (عربية). يُحدد السياق عبر طريقة localizeString(fromTimeInterval:) أو مباشرة من خلال string(from: Date).
RelativeDateTimeFormatter يوفر عدة إعدادات للتحكم في المخرجات: خاصية unitsStyle تحدد نمط التنسيق (numeric, abbreviated, full, spellOut)، و maximumUnitCount يحد عدد الوحدات المعروضة. على سبيل المثال، مع maximumUnitCount = 1، يتم عرض فرق ساعة و30 دقيقة كـ «قبل ساعة» بدلاً من «قبل ساعة و30 دقيقة».
تحديد الوحدات: افتراضيًا، يعرض RelativeDateTimeFormatter وحدة واحدة فقط (الأكبر). ضبط maximumUnitCount = 2 يضيف الوحدة التالية لوصف أكثر دقة: «قبل ساعة و30 دقيقة». ومع ذلك، قد يجعل ذلك السلسلة طويلة جدًا للرسائل القصيرة (الإشعارات الفورية، التنبيهات). لواجهة المستخدم، يُوصى بالإبقاء على 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()))")
اختيار النمط لسياقات مختلفة: لخلاصة الأخبار، استخدم .numeric مع maximumUnitCount = 1 — هذا هو المعيار لتويتر وإنستغرام وفيسبوك. لإمكانية الوصول (VoiceOver)، استخدم .full — الأرقام المكتوبة تُقرأ بشكل أكثر طبيعية. للعناصر المضغوطة (شارة الإشعار، شريط الحالة)، استخدم .abbreviated لتوفير المساحة.
الاستخدام الأساسي لـ RelativeDateTimeFormatter يتلخص في إنشاء مثيل، تكوين الخصائص، واستدعاء إحدى طرق التنسيق. الطرق الرئيسية: 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 ثانية»، وهو أمر غير جيد في واجهة المستخدم. يُوصى بتغليف استدعاء المنسق في منطق شرطي: إذا كان الفرق أقل من حد معين (مثل 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، الإعدادات المحلية غير ثابتة ويمكن تغييرها لكل استدعاء. هذا يسمح بعرض التواريخ النسبية بلغة مختلفة عن لغة الواجهة (مثل المحتوى بلغته الأصلية).
يكمن تعقيد توطين التواريخ النسبية في الخصائص النحوية للغات المختلفة. اللغة العربية تتطلب صيغًا مختلفة للأرقام: «دقيقة واحدة»، «دقيقتان»، «5 دقائق». اللغة الصينية ليس لديها تصريف على الإطلاق، مما يبسط المهمة. يغطي 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 Arabic 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 يتجاهل TimeZone عند حساب الفرق لإعدادات .numeric — يستخدم الفرق المطلق بالثواني. ومع ذلك، لنمط .full (بأرقام مكتوبة) والحالات الخاصة (أمس، اليوم)، يتم أخذ TimeZone في الاعتبار. قم دائمًا بتعيين TimeZone بشكل صريح لتحقيق الاتساق، خاصة إذا كان التطبيق يعمل مع تواريخ الخادم بتوقيت UTC.
تجاهل TimeZone عند حساب التواريخ النسبية — خطأ شائع عند العمل مع تواريخ الخادم. إذا أرسل الخادم Date بتوقيت UTC، واستخدم RelativeDateTimeFormatter TimeZone.current، فقد يتم حساب الفرق بشكل غير صحيح للتواريخ القريبة من اللحظة الحالية. يُوصى دائمًا بتعيين formatter.timeZone = TimeZone(secondsFromGMT: 0) لبيانات الخادم.
اختيار غير صحيح للوحدة للفترات القصيرة — RelativeDateTimeFormatter يُقرّب الفرق إلى أكبر وحدة. لـ 25 ساعة، ستكون النتيجة «قبل يوم»، مما قد يضلل المستخدم. إذا كانت الدقة العالية مطلوبة (مثل عدادات التنازل)، استخدم DateComponentsFormatter بدلاً من RelativeDateTimeFormatter — يسمح بعرض وحدات متعددة في وقت واحد.
عدم التحقق من TimeInterval السالب — إذا تم تمرير تاريخ مستقبلي كماضٍ (قيمة سالبة في string(fromTimeInterval:))، فقد يعيد المنسق سلسلة غير صحيحة. تحقق دائمًا من إشارة الفاصل الزمني قبل تمريره إلى المنسق، خاصة عند العمل مع بيانات الخادم حيث قد تشوه المنطقة الزمنية الحساب.
وفقًا Hacker News (2024)، واحدة من أكثر المشكلات التي تمت مناقشتها حول RelativeDateTimeFormatter هي عدم وجود دعم مدمج لـ «أمس» و«اليوم» للغة الإنجليزية. بدلاً من «أمس»، المنسق لفرق 90000 ثانية يعيد «قبل يوم». بالنسبة للعربية لا توجد هذه المشكلة — «قبل يوم» يبدو طبيعيًا، لكن بالنسبة لواجهة المستخدم الإنجليزية، «yesterday» أفضل. هذه الوظيفة غير مدعومة وتتطلب فحصًا يدويًا عبر Calendar.isDateInToday/Yesterday.
الأسئلة المتكررة
RelativeDateTimeFormatter هي كلاس Foundation لعرض التواريخ بتنسيق نسبي: «قبل 5 دقائق»، «بعد يومين». متاح من iOS 13 وmacOS 10.15.
بمبدأ الوحدة غير الصفرية الأكبر — ثوانٍ، دقائق، ساعات، أيام، أسابيع، أشهر أو سنوات. على سبيل المثال، لفرق 3720 ثانية (ساعة ودقيقتان)، يتم اختيار وحدة «ساعة»، وليس «دقائق».
قم بتعيين الخاصية locale إلى مثيل Locale المطلوب. افتراضيًا يُستخدم Locale.current. مثال: formatter.locale = Locale(identifier: "de_DE") للألمانية.
.numeric — الصيغة الكاملة («قبل 3 أيام»)، .abbreviated — الصيغة المختصرة («قبل 3 أ.»). يعتمد الاختيار على السياق: numeric لواجهة المستخدم الرئيسية، abbreviated للعناصر المضغوطة.
أضف تحققًا يدويًا لفاصل زمني أقل من 5-10 ثوانٍ. RelativeDateTimeFormatter لا يدعم «للتو» — للفترات الصغيرة يعيد «قبل 0 ثانية». استخدم منطقًا شرطيًا مع حد.
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا