# Backend-Driven UI: рекомендации в iOS

Используйте этот сценарий, когда приложение само рисует виджет, а сервер управляет его структурой, стилем и подборкой товаров. Загрузите контент по selector, сохраните campaign/content/slot для engagement и передайте структуру собственному рендереру. [Общие правила загрузки](./custom_ui.md).

## Структура виджета

| Поле | Как использовать |
|---|---|
| `response.data[*].payload[*].contents[*]` | Выберите блок кампании, который будет отображаться |
| `content.variables?.frameUI?.container` | Внешний контейнер: фон, скругление, padding, margin, size и onClick |
| `content.variables?.elements` | Элементы в порядке, заданном сервером |
| `products-container` | Место вставки товарного блока |
| `element.style.productContainerType` | Layout `row` или `grid` |
| `element.style.gridColumns` | Число колонок; если отсутствует, используйте дефолт клиента |
| `content.products?.slots` | Товары в порядке ранжирования сервера |
| `slot.item` | Поля товара из вашего фида |

`products.strategyId`, `products.name` и `products.fallback` описывают выбранную стратегию. Сохраняйте порядок товаров и элементов; не подменяйте серверный заголовок или layout локальными значениями.

## Поддерживаемые элементы

| Элемент / действие | Встроенный SDK | Рендерер из примера |
|---|---|---|
| `text`, `image`, `button`, `spacer` | Поддерживает | Поддерживает |
| `products-container`, row/grid | Поддерживает row через ProductViewBuilder; grid не отображается | Рисует собственные карточки в row и grid |
| `items-container`, `webview` | Поддерживает | Требует дополнительной реализации |
| URL, deeplink, copy | Поддерживает; навигация передаётся в callback | Обрабатывает через переданные функции и clipboard |
| `openStep` | Переключает шаг кампании | Требует дополнительной реализации |

Пример применяет backgroundColor, cornerRadius, padding, margin, size, fontSize, fontWeight, textColor, contentAlignment, src, fit и параметры товарного layout. Добавляйте новые элементы и действия явно; для неизвестных типов определите безопасный fallback. `frameUI.container.onClick` можно подключить к wrapper через тот же обработчик действий, что для element.onClick.

## Подключите полный пример

Скачайте [server_driven_widget.swift](./examples/server_driven_widget.swift) и добавьте файл в проект с SDK. Кампания для примера содержит один корневой блок без дополнительных шагов. Передайте обычное имя selector, PageContext и обработчики открытия URL, deeplink и товара.

SwiftUI-пример требует **iOS 15**, поскольку использует AsyncImage. Для iOS 14 замените загрузчик изображения. В этой среде проверен синтаксис Swift; полная сборка с iOS SDK требует Xcode.

```swift
ServerDrivenRecommendationWidget(
    selector: "homepage-recommendations",
    pageContext: pageContext,
    onOpenUrl: openUrl,
    onOpenDeeplink: openDeeplink,
    onOpenProduct: openProduct
)
```

## Аналитика и ограничения

Пример вручную отправляет impression после появления виджета, visible impression — при видимости минимум 50% в течение секунды, клик товара — до навигации. Видимые показы дедуплицируются для размещения. Detector сравнивает геометрию с viewport и не учитывает перекрывающие слои.

Не передавайте decisionId или tracking URL отдельно: используйте campaign/content/slot из ответа. Ручной engagement не вызывает gravityEventCallback. [Типы engagement и условия отправки](./custom_ui.md#аналитика-собственного-ui).

## Проверка

1. Переключите row/grid, заголовок и порядок elements в кампании: после новой загрузки UI должен использовать серверную конфигурацию.
2. Прокрутите виджет, нажмите товар и проверьте engagement без дублирования из callback.
3. Проверьте пустой ответ, ошибку загрузки и отсутствующие поля: виджет должен безопасно скрыться.
4. Не добавляйте неподдерживаемые элементы или дополнительные шаги без расширения рендерера.
