# Свой UI и Custom JSON в iOS

Используйте этот путь, когда приложение само рисует кампанию или рекомендации. SDK получает контент и отправляет content load tracking; видимость, клики и навигацию определяет приложение. Сначала выполните [инициализацию](./configuration.md).

## `getContentBySelector(selector:pageContext:)`

Используйте этот метод для manual rendering и кастомного UI.

```swift
let response = await GravitySDK.instance.getContentBySelector(
    selector: "homepage-recommendations",
    pageContext: PageContext(
        type: .homepage,
        data: [],
        location: "app://homepage"
    )
)

if let response {
    // Разберите response.data и постройте собственный UI.
}
```

Для вызова требуется корректно заполненный [`PageContext`](./events.md#pagecontext).

Что важно учесть:

- успешный `getContentBySelector(...)` автоматически запускает content load tracking для всех `contents`, пришедших в response;
- при сетевой ошибке или ошибке декодирования метод возвращает `nil` и пишет SDK-лог;
- это означает, что `ContentLoadEvent` может прийти в `gravityEventCallback` сразу после загрузки контента, даже если UI еще не был показан пользователю;
- impression / visible impression / click для manual widget SDK автоматически не отправляет: приложение по-прежнему должно вызвать `sendContentEngagement(...)` и `sendProductEngagement(...)` самостоятельно.

## Загрузка по группе и ID кампании

Для группы и ID кампании используйте `getContentByGroupSelector(...)` и `getContentByCampaignId(...)`: [сигнатуры](./api_reference.md#загрузка-контента).

Методы загрузки по группе и ID кампании возвращают ту же структуру `ContentResponse?`, автоматически отправляют content load tracking и не показывают UI. Все методы выбора контента используют `Options` и `ContentSettings`, установленные через `setOptions(...)`.

## Custom JSON

Пользовательский JSON приходит в `content.custom?.json` как строка. SDK не декодирует её в модель приложения. Например, для кампании с JSON `{ "variant": "compact" }`:

```swift
struct ExperimentConfig: Decodable {
    let variant: String
}

if let campaign = response?.data.first,
   let content = campaign.payload.first?.contents.first,
   let json = content.custom?.json,
   let data = json.data(using: .utf8) {
    do {
        let config = try JSONDecoder().decode(ExperimentConfig.self, from: data)
        // Покажите UI для config.variant. Для неизвестного значения используйте свой fallback.
    } catch {
        // Используйте стандартный UI приложения.
    }
}
```

Вариант A/B выбирает сервер; приложение использует полученный JSON и сохраняет исходные `campaign`/`content` для engagement. [Полный сценарий A/B](./ab_testing.md).

Отдельного параметра `headless` у native SDK нет. `getContentBySelector(...)` сам не показывает UI, но `trackView(...)` и `triggerEvent(...)` могут запустить автопоказ. Для сценария со своим UI используйте selector/Custom JSON кампании; presentation lock ограничивает новые in-app показы, но не отменяет HTTP-запросы и content load tracking.

## Аналитика собственного UI

Загрузка не означает показ. Сохраните исходные campaign/content и при работе с товарами slot. Передавайте их в engagement после фактического взаимодействия:

| Действие | Тип engagement |
|---|---|
| Контент появился в интерфейсе | `ContentImpressionEngagement` |
| Контент достиг порога видимости | `ContentVisibleImpressionEngagement` |
| Контент закрыт | `ContentCloseEngagement` |
| Карточка товара достигла порога видимости | `ProductVisibleImpressionEngagement` |
| Пользователь нажал на товар | `ProductClickEngagement` |

Порог и дедупликацию реализует приложение. Встроенный iOS detector использует 50% площади без задержки; в полном примере собственного рендерера выбрано 50% в течение секунды.

Публичного ContentClickEngagement нет. sendContentEngagement/sendProductEngagement вызывают tracking URL, но не gravityEventCallback. Для бизнес-действий отправляйте TriggerEvent отдельно. [Справочник engagement](./api_reference.md#engagement).
