RelativeDateTimeFormatter: الجوهر، التواريخ النسبية وSwift

المؤلف: IT Sectr نُشر: 2026-07-13 وقت القراءة: 10 دق

RelativeDateTimeFormatter هي كلاس في Foundation في iOS وmacOS تحول التواريخ المطلقة إلى صيغ نسبية قابلة للقراءة: «قبل 5 دقائق»، «أمس»، «بعد 3 أيام». وفقًا Apple Developer Documentation, 2024، يقوم RelativeDateTimeFormatter تلقائيًا باختيار الوحدة المناسبة (ثوانٍ، دقائق، ساعات، أيام) ويوطّن المخرجات بلغة الإعدادات المحلية الحالية للجهاز. على عكس الحساب اليدوي للفرق بين التواريخ عبر Calendar، تراعي هذه الكلاس الخصائص اللغوية لكل لغة: بالنسبة لبعض اللغات تُصرف الأرقام، وبالنسبة لأخرى تُستخدم صيغة خاصة لكلمة «أمس». الكلاس متاح بدءًا من iOS 13 وmacOS 10.15.

الخلاصة

  • RelativeDateTimeFormatter — كلاس لعرض التواريخ النسبية في iOS وmacOS (iOS 13+)
  • مخرجات محلية — يختار تلقائيًا الصيغ بلغة الإعدادات المحلية الحالية
  • ثلاثة أنواع من السياق — الماضي (قبل)، المستقبل (بعد)، الحاضر (الآن) مع صيغ مختلفة
  • اختيار تلقائي للوحدة — ثوانٍ، دقائق، ساعات، أيام، أسابيع، أشهر، سنوات
  • تخصيص النمط — numeric (بعد 3 أيام) أو abbreviated (بعد 3 أيام)

ما هو RelativeDateTimeFormatter؟

RelativeDateTimeFormatter هي فئة فرعية من Formatter في Foundation تأخذ Date (أو فرقًا بالثواني) وتعيد سلسلة محلية مع الوقت النسبي. على سبيل المثال، لتاريخ قبل 5 دقائق من التاريخ الحالي، تعيد «قبل 5 دقائق». تدعم الكلاس ثلاثة سياقات زمنية: الماضي والمستقبل والحاضر.

يستخدم المنطق الداخلي لـ RelativeDateTimeFormatter كلاً من Calendar وLocale لحساب الفرق بين التواريخ واختيار الصيغة النحوية الصحيحة. بالنسبة للعربية، يختار بين «قبل دقيقة» و«قبل دقيقتين» و«قبل 5 دقائق». تستند هذه الوظيفة إلى بيانات ICU (المكونات الدولية لـ Unicode) ولا تتطلب أي تكوين إضافي من المطور.

وفقًا Apple WWDC 2019، أصبح RelativeDateTimeFormatter جزءًا من الإطار لتبسيط التوطين — قبل ظهوره، كان المطورون مضطرين لحساب فرق التواريخ يدويًا واستبدال السلاسل المحلية عبر String.localizedStringWithFormat. مما أدى إلى أخطاء في التصريف (خاصة للغات السلافية والعربية) واختيار غير صحيح لوحدات القياس.

كيف يعرض RelativeDateTimeFormatter «قبل 5 دقائق»؟

الخوارزمية لـ 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 دقيقة».

أنماط التنسيق

  • .numeric — القيمة الرقمية الكاملة: «قبل 3 أيام»، «بعد أسبوعين». موصى به لواجهة المستخدم افتراضيًا
  • .abbreviated — صيغة مختصرة: «قبل 3 أ.»، «بعد 2 أ.». للعرض المضغوط في الجداول والقوائم
  • .full — صيغة لفظية بدون أرقام: «قبل ثلاثة أيام». لإمكانية الوصول وواجهات الصوت
  • .spellOut — صيغة حرفية بهجاء بديل: «قبل ثلاثة أيام». نادر الاستخدام، بشكل رئيسي للاستخدامات المتخصصة

تحديد الوحدات: افتراضيًا، يعرض RelativeDateTimeFormatter وحدة واحدة فقط (الأكبر). ضبط maximumUnitCount = 2 يضيف الوحدة التالية لوصف أكثر دقة: «قبل ساعة و30 دقيقة». ومع ذلك، قد يجعل ذلك السلسلة طويلة جدًا للرسائل القصيرة (الإشعارات الفورية، التنبيهات). لواجهة المستخدم، يُوصى بالإبقاء على maximumUnitCount = 1.

swift
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 في Swift: أمثلة

الاستخدام الأساسي لـ RelativeDateTimeFormatter يتلخص في إنشاء مثيل، تكوين الخصائص، واستدعاء إحدى طرق التنسيق. الطرق الرئيسية: localizedString(for:relativeTo:) — لزوج من التواريخ، localizedString(fromTimeInterval:) — للفرق بالثواني، و string(for:) — لـ Date مع سياق تلقائي (ماضي/مستقبل).

swift
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 ثوانٍ) — عرض «للتو» يدويًا، وإلا تمرير التاريخ إلى المنسق.

swift
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، دون الحاجة إلى كود إضافي.

swift
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؟

RelativeDateTimeFormatter هي كلاس Foundation لعرض التواريخ بتنسيق نسبي: «قبل 5 دقائق»، «بعد يومين». متاح من iOS 13 وmacOS 10.15.

كيف يختار RelativeDateTimeFormatter الوحدات؟

بمبدأ الوحدة غير الصفرية الأكبر — ثوانٍ، دقائق، ساعات، أيام، أسابيع، أشهر أو سنوات. على سبيل المثال، لفرق 3720 ثانية (ساعة ودقيقتان)، يتم اختيار وحدة «ساعة»، وليس «دقائق».

كيف تغير لغة المخرجات؟

قم بتعيين الخاصية locale إلى مثيل Locale المطلوب. افتراضيًا يُستخدم Locale.current. مثال: formatter.locale = Locale(identifier: "de_DE") للألمانية.

ما الفرق بين .numeric و .abbreviated؟

.numeric — الصيغة الكاملة («قبل 3 أيام»)، .abbreviated — الصيغة المختصرة («قبل 3 أ.»). يعتمد الاختيار على السياق: numeric لواجهة المستخدم الرئيسية، abbreviated للعناصر المضغوطة.

كيف تعرض «للتو» بدلاً من «قبل 0 ثانية»؟

أضف تحققًا يدويًا لفاصل زمني أقل من 5-10 ثوانٍ. RelativeDateTimeFormatter لا يدعم «للتو» — للفترات الصغيرة يعيد «قبل 0 ثانية». استخدم منطقًا شرطيًا مع حد.

الملخص

  • RelativeDateTimeFormatter — كلاس مناسب لعرض التواريخ النسبية في iOS 13+
  • توطين تلقائي — تصريف صحيح لجميع اللغات المدعومة عبر ICU
  • ثلاثة أنماط — .numeric (قياسي)، .abbreviated (مضغوط)، .full (كتابة)
  • اختيار الوحدة — تلقائي بناءً على أكبر قيمة غير صفرية
  • إعداد TimeZone — إلزامي للاتساق عند العمل مع تواريخ الخادم
  • حد «للتو» — غير مدعوم مدمجًا؛ يتطلب فحصًا يدويًا للفاصل الزمني
  • لا دعم لـ «أمس» — المنسق لا يستخدم صيغة yesterday للإنجليزية

سنقوم بتطوير تطبيق جوال جاهز

تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.

مناقشة المشروع

اقرأ أيضًا