# Push Stories для iOS

Push stories превращают расширенное push-уведомление в полноэкранный просмотр историй в стиле Instagram: изображения без полей, индикаторы выполнения вверху, автоматическое переключение страниц, навигация касанием и кнопка с deep link'ом. Они отображаются с помощью Notification Content Extension, используя автономный модуль `PushwooshNotificationUI` — вы создаете подкласс одного view controller'а, а SDK обрабатывает парсинг, загрузку изображений, прогресс, тайминги, навигацию и deep link'и.

Доступно с версии 7.0.46.

<img src="/ios-push-stories-demo.webp" alt="Воспроизведение push stories в расширенном уведомлении"/>

## 1. Добавьте Notification Content Extension

В Xcode выберите **File > New > Target…**, затем **Notification Content Extension** и назовите его (например, **StoriesContentExtension**).

<img src="/ios-push-stories-1.webp" alt="Добавление цели Notification Content Extension в Xcode"/>

## 2. Добавьте модуль PushwooshNotificationUI

`PushwooshNotificationUI` — это автономный модуль без других зависимостей от Pushwoosh, поэтому он остается небольшим в процессе расширения. Добавьте его в **цель расширения контента** (content extension target), а не в цель приложения (app target).

**Swift Package Manager**

<img src="/ios-push-stories-spm.webp" alt="Добавление пакета PushwooshNotificationUI в цель расширения"/>

**CocoaPods**

```ruby
target 'StoriesContentExtension' do
  use_frameworks!

  pod 'PushwooshXCFramework/PushwooshNotificationUI'
end
```

## 3. Создайте подкласс для view controller'а историй

Замените сгенерированное тело `NotificationViewController` подклассом `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` должен совпадать с категорией, которую вы отправляете в push-уведомлении (`PW_STORIES`). `UNNotificationExtensionInitialContentSizeRatio` устанавливает начальное соотношение высоты расширенного представления.
</Aside>

## 5. Отправьте push-уведомление с историями

Отправьте уведомление, категория которого — `PW_STORIES`, а пользовательские данные (custom data) содержат блок `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>

## Кастомизация

Переопределите свойства в вашем подклассе, чтобы настроить опыт взаимодействия. У всех свойств есть разумные значения по умолчанию.

<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` | Плавный переход между страницами вместо резкой смены. При включенном режиме «Уменьшение движения» используется мгновенная смена. |
| `loopsAfterLastPage` | `false` | Начинать с первой страницы после завершения последней. |
| `appGroupIdentifier` | `nil` | App Group, используемая совместно с Notification Service Extension для предварительного кэширования медиа (см. ниже). |

Вы также можете переопределить `showDefaultContent(for:)`, чтобы настроить резервный вариант, который будет показан, если полезная нагрузка отсутствует или некорректна (по умолчанию показывается тело уведомления).

### Предварительное кэширование медиа

Для мгновенного отображения первого кадра в офлайн-режиме используйте общую App Group для вашего Content Extension и Notification Service Extension. Переопределите `appGroupIdentifier` в контроллере историй, а затем предварительно загрузите медиа из `didReceive(_:withContentHandler:)` вашего Service Extension:

<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`, чтобы Service Extension запустился. Без App Group медиафайлы кэшируются в каталоге `tmp` расширения.

### Колбэки жизненного цикла и аналитики

Установите `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>

| Колбэк | Когда срабатывает |
|----------|----------------|
| `didStartWithPageCount:` | Была проанализирована корректная полезная нагрузка историй, и воспроизведение вот-вот начнется. |
| `didShow:at:` | Страница стала видимой. Используйте это для отслеживания показов каждой страницы. |
| `didTapActionFor:at:` | Пользователь нажал на кнопку призыва к действию. |
| `storiesViewControllerDidFinish:` | Воспроизведение последней страницы завершилось. |
| `storiesViewControllerDidShowFallback:` | Полезная нагрузка отсутствовала или была некорректной, и было показано резервное содержимое. |

`StoryPage`, передаваемый в колбэки, предоставляет доступ к `imageURL`, `duration`, `link`, `buttonTitle`, `title` и `subtitle` страницы.

### Навигация

Нажмите на правую треть экрана, чтобы перейти к следующей странице, и на левую треть, чтобы вернуться назад. Горизонтальный свайп намеренно не используется, так как он конфликтует с системным жестом закрытия уведомления. Нажатие на кнопку страницы открывает ее deep link и закрывает уведомление.

## Ссылки

<CardGrid>
  <LinkCard
    title="Справочник по API iOS SDK"
    description="Полная техническая документация, охватывающая все публичные классы, методы и свойства."
    href="https://pushwoosh.github.io/pushwoosh-ios-sdk/"
  />
  <LinkCard
    title="Messages API"
    description="Отправляйте уведомления, включая поля data и ios_category_custom, через Pushwoosh API."
    href="/developer/api-reference/messages-api/"
  />
</CardGrid>