# قصص الإشعارات الفورية لنظام iOS

تحول قصص الإشعارات الفورية (Push stories) الإشعار الفوري الموسع إلى تجربة قصص بملء الشاشة بأسلوب Instagram: صور تملأ الشاشة بالكامل، أشرطة تقدم في الأعلى، صفحات تتقدم تلقائيًا، النقر للتنقل، وزر مع رابط عميق (deep link). يتم عرضها بواسطة ملحق محتوى الإشعارات (Notification Content Extension) باستخدام وحدة `PushwooshNotificationUI` المستقلة — كل ما عليك هو عمل subclass لوحدة تحكم عرض واحدة (view controller) ويتولى SDK التعامل مع التحليل (parsing)، تحميل الصور، التقدم، التوقيت، التنقل، والروابط العميقة.

متوفر منذ الإصدار 7.0.46.

<img src="/ios-push-stories-demo.webp" alt="عرض قصص الإشعارات الفورية في إشعار موسع"/>

## 1. إضافة ملحق محتوى الإشعارات (Notification Content Extension)

في Xcode، اختر **File > New > Target…**، حدد **Notification Content Extension**، وقم بتسميته (على سبيل المثال **StoriesContentExtension**).

<img src="/ios-push-stories-1.webp" alt="إضافة هدف ملحق محتوى الإشعارات في Xcode"/>

## 2. إضافة وحدة PushwooshNotificationUI

`PushwooshNotificationUI` هي وحدة مستقلة لا تعتمد على أي وحدات أخرى من Pushwoosh، لذا يبقى حجمها صغيرًا داخل عملية الملحق. أضفها إلى **هدف ملحق المحتوى** (وليس هدف التطبيق).

**Swift Package Manager**

<img src="/ios-push-stories-spm.webp" alt="إضافة حزمة PushwooshNotificationUI إلى هدف الملحق"/>

**CocoaPods**

```ruby
target 'StoriesContentExtension' do
  use_frameworks!

  pod 'PushwooshXCFramework/PushwooshNotificationUI'
end
```

## 3. عمل Subclass لوحدة تحكم عرض القصص

استبدل محتوى `NotificationViewController` الذي تم إنشاؤه بـ subclass من `PushwooshStoriesViewController`. هذا هو كل التكامل المطلوب.

<Tabs>
<TabItem label="Swift">
```swift
import PushwooshNotificationUI

class NotificationViewController: PushwooshStoriesViewController {}
```
</TabItem>
</Tabs>

## 4. تكوين Info.plist للملحق

في ملف **Info.plist** الخاص بملحق المحتوى، قم بتعيين المفاتيح التالية تحت `NSExtension > NSExtensionAttributes`:

```xml
<key>UNNotificationExtensionCategory</key>
<string>PW_STORIES</string>
<key>UNNotificationExtensionUserInteractionEnabled</key>
<true/>
<key>UNNotificationExtensionDefaultContentHidden</key>
<true/>
<key>UNNotificationExtensionInitialContentSizeRatio</key>
<real>1.5</real>
```

<Aside type="note">
يجب أن يتطابق `UNNotificationExtensionCategory` مع الفئة التي ترسلها في الإشعار (`PW_STORIES`). يحدد `UNNotificationExtensionInitialContentSizeRatio` نسبة الارتفاع الأولية للعرض الموسع.
</Aside>

## 5. إرسال إشعار قصص فورية

أرسل إشعارًا تكون فئته `PW_STORIES` وتحمل بياناته المخصصة كتلة `pw_stories`. استخدم حقل `ios_category_custom` المخصص للفئة وحقل `data` لحمولة القصص.

```json
{
  "request": {
    "application": "APPLICATION_CODE",
    "auth": "API_ACCESS_TOKEN",
    "notifications": [
      {
        "send_date": "now",
        "content": "Tap to explore",
        "ios_title": "Push Stories",
        "ios_category_custom": "PW_STORIES",
        "ios_root_params": {
          "aps": {
            "mutable-content": 1
          }
        },
        "data": {
          "pw_stories": {
            "pages": [
              {
                "image": "https://example.com/story-1.jpg",
                "duration": 5.0,
                "link": "yourapp://page1",
                "button_title": "Get started",
                "title": "Welcome",
                "subtitle": "Swipe to explore what's new"
              },
              {
                "image": "https://example.com/story-2.jpg",
                "duration": 4.0,
                "link": "yourapp://page2",
                "button_title": "Learn more",
                "title": "Stay in the loop",
                "subtitle": "Updates, tips and more"
              }
            ]
          }
        }
      }
    ]
  }
}
```

تدعم كل صفحة الحقول التالية. حقل `image` فقط هو المطلوب؛ والباقي اختياري.

| الحقل | الوصف |
|-------|-------------|
| `image` | رابط URL للصورة بملء الشاشة للصفحة. |
| `duration` | عدد الثواني التي تظل فيها الصفحة على الشاشة قبل التقدم التلقائي. القيمة الافتراضية حوالي 5 ثوانٍ. |
| `link` | رابط عميق (Deep link) يتم فتحه عند النقر على زر الصفحة. |
| `button_title` | عنوان زر الصفحة. |
| `title` | نص العنوان الذي يظهر فوق الصفحة. |
| `subtitle` | نص العنوان الفرعي الذي يظهر فوق الصفحة. |

<Aside type="note">
كتلة `pw_stories` المفقودة أو الفارغة أو غير الصحيحة لا تؤدي إلى تعطل الملحق — بل يعود إلى محتوى الإشعار الافتراضي. `mutable-content: 1` مطلوب فقط لمسار التخزين المؤقت المسبق للوسائط الاختياري الموضح أدناه.
</Aside>

## التخصيص

قم بتجاوز (Override) الخصائص في الـ subclass الخاص بك لتعديل التجربة. جميعها لها قيم افتراضية معقولة.

<Tabs>
<TabItem label="Swift">
```swift
import PushwooshNotificationUI

class NotificationViewController: PushwooshStoriesViewController {
    override var storyAspectRatio: CGFloat { 1.5 }     // keep in sync with InitialContentSizeRatio
    override var hapticsEnabled: Bool { true }
    override var longPressToPauseEnabled: Bool { true }
    override var crossfadesBetweenPages: Bool { true }
    override var loopsAfterLastPage: Bool { false }
}
```
</TabItem>
</Tabs>

| الخاصية | الافتراضي | الوصف |
|----------|---------|-------------|
| `storyAspectRatio` | `1.5` | نسبة العرض إلى الارتفاع (الارتفاع ÷ العرض) لمنطقة القصص. حافظ على تزامنها مع `UNNotificationExtensionInitialContentSizeRatio`. |
| `hapticsEnabled` | `false` | تشغيل نقرة لمسية خفيفة عند التنقل في منطقة النقر. |
| `longPressToPauseEnabled` | `false` | الضغط مع الاستمرار يوقف الصفحة الحالية؛ ورفع الإصبع يستأنفها. |
| `crossfadesBetweenPages` | `false` | تلاشي متقاطع بين الصفحات بدلاً من الانتقال الحاد. يعود إلى التغيير الفوري عند تشغيل تقليل الحركة (Reduce Motion). |
| `loopsAfterLastPage` | `false` | إعادة التشغيل من الصفحة الأولى بعد انتهاء الصفحة الأخيرة. |
| `appGroupIdentifier` | `nil` | مجموعة التطبيقات (App Group) مشتركة مع ملحق خدمة الإشعارات (Notification Service Extension) للتخزين المؤقت المسبق للوسائط (انظر أدناه). |

يمكنك أيضًا تجاوز (override) `showDefaultContent(for:)` لتخصيص المحتوى الاحتياطي الذي يظهر عندما تكون الحمولة مفقودة أو غير صحيحة (بشكل افتراضي، يعرض محتوى التنبيه).

### التخزين المؤقت المسبق للوسائط

للحصول على إطار أول فوري وغير متصل بالإنترنت، شارك مجموعة تطبيقات (App Group) بين ملحق المحتوى الخاص بك وملحق خدمة الإشعارات (Notification Service Extension). قم بتجاوز `appGroupIdentifier` في وحدة تحكم القصص، ثم قم بتنزيل الوسائط مسبقًا من `didReceive(_:withContentHandler:)` في ملحق الخدمة الخاص بك:

<Tabs>
<TabItem label="Swift">
```swift
import PushwooshNotificationUI

PushwooshStoriesMediaPrefetcher.prefetch(
    userInfo: request.content.userInfo,
    appGroupIdentifier: "group.com.example.app"
) {
    contentHandler(bestAttemptContent)
}
```
</TabItem>
</Tabs>

قم بتمكين قدرة **App Groups** على كلا الملحقين بنفس معرف المجموعة، وأرسل `mutable-content: 1` حتى يعمل ملحق الخدمة. بدون مجموعة تطبيقات، يتم تخزين الوسائط مؤقتًا في دليل `tmp` الخاص بالملحق بدلاً من ذلك.

### دورة الحياة وردود الاتصال التحليلية (Lifecycle and analytics callbacks)

قم بتعيين `storiesDelegate` لمراقبة أحداث القصة — مرات ظهور الصفحة، نقرات الأزرار، الإكمال، والمحتوى الاحتياطي. يجب أن يتوافق مع `PushwooshStoriesDelegate`؛ كل دالة اختيارية، لذا قم بتنفيذ الدوال التي تحتاجها فقط.

<Tabs>
<TabItem label="Swift">
```swift
import PushwooshNotificationUI

class NotificationViewController: PushwooshStoriesViewController, PushwooshStoriesDelegate {
    override func viewDidLoad() {
        super.viewDidLoad()
        storiesDelegate = self
    }

    func storiesViewController(_ controller: PushwooshStoriesViewController, didStartWithPageCount pageCount: Int) {}
    func storiesViewController(_ controller: PushwooshStoriesViewController, didShow page: StoryPage, at index: Int) {}
    func storiesViewController(_ controller: PushwooshStoriesViewController, didTapActionFor page: StoryPage, at index: Int) {}
    func storiesViewControllerDidFinish(_ controller: PushwooshStoriesViewController) {}
    func storiesViewControllerDidShowFallback(_ controller: PushwooshStoriesViewController) {}
}
```
</TabItem>
</Tabs>

| رد الاتصال (Callback) | متى يتم تفعيله |
|----------|----------------|
| `didStartWithPageCount:` | تم تحليل حمولة قصص صالحة والتشغيل على وشك البدء. |
| `didShow:at:` | أصبحت الصفحة مرئية. استخدمها لتسجيل مرات الظهور لكل صفحة. |
| `didTapActionFor:at:` | نقر المستخدم على زر الحث على اتخاذ إجراء. |
| `storiesViewControllerDidFinish:` | انتهى تشغيل الصفحة الأخيرة. |
| `storiesViewControllerDidShowFallback:` | كانت الحمولة مفقودة أو غير صحيحة وتم عرض المحتوى الاحتياطي. |

الكائن `StoryPage` الذي يتم تمريره إلى ردود الاتصال يكشف عن `imageURL`، `duration`، `link`، `buttonTitle`، `title`، و `subtitle` للصفحة.

### التنقل

انقر على الثلث الأيمن من الشاشة للانتقال إلى الصفحة التالية والثلث الأيسر للعودة. لم يتم استخدام السحب الأفقي عن قصد، لأنه يتعارض مع إيماءة النظام لرفض الإشعارات. النقر على زر الصفحة يفتح الرابط العميق الخاص به ويرفض الإشعار.

## المراجع

<CardGrid>
  <LinkCard
    title="مرجع واجهة برمجة تطبيقات iOS SDK"
    description="توثيق فني كامل يغطي جميع الفئات والأساليب والخصائص العامة."
    href="https://pushwoosh.github.io/pushwoosh-ios-sdk/"
  />
  <LinkCard
    title="واجهة برمجة تطبيقات الرسائل (Messages API)"
    description="إرسال الإشعارات، بما في ذلك حقلي data و ios_category_custom، من خلال واجهة برمجة تطبيقات Pushwoosh."
    href="/developer/api-reference/messages-api/"
  />
</CardGrid>