# Конфигурация iOS SDK

Инициализируйте SDK один раз при старте приложения. [Установка и минимальное приложение](./overview.md); точные сигнатуры собраны в [справочнике API](./api_reference.md).

## Инициализация

Параметры:

- `apiKey` — API key проекта.
- `section` — идентификатор секции проекта.
- `gravityEventCallback` — опциональный callback для событий SDK и действий пользователя внутри кампаний.
- `productViewBuilder` — шаблон карточки товара, который приложение передает SDK. Обязателен для отображения товарных рекомендаций.
- `productFilter` — клиентский фильтр слотов товаров. SDK применяет его при декодировании `Products.slots`, поэтому отфильтрованные товары не попадут в готовые product-based блоки и manual response.
- `uiSettings` — опциональные настройки UI SDK, например кастомный шрифт через `UISettings(fontName:)`.
- `logLevel` — уровень логирования внутреннего SDK-логгера. По умолчанию `.error`.

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

- `initialize(...)` рекомендуется выполнить до первого обращения к `GravitySDK.instance`;
- `gravityEventCallback` используется для обработки действий пользователя внутри кампаний и tracking-событий поддерживаемых сценариев;
- если в приложении отображаются товарные рекомендации, передайте `productViewBuilder` уже на этапе инициализации.

## Кастомный шрифт и элементы UI

Зарегистрируйте шрифт в приложении и передайте его PostScript name:

```swift
GravitySDK.initialize(
    apiKey: "YOUR_API_KEY",
    section: "YOUR_SECTION_ID",
    uiSettings: UISettings(fontName: "MyFont-Regular")
)
```

Если `UIFont` не сможет загрузить шрифт, SDK использует системный и пишет error-лог. Встроенный рендерер поддерживает текст, изображения, кнопки, spacer, товарные контейнеры, `items-container` и `webview` с URL в `src`. Действие `openStep` переключает контент по номеру `step` в кампании; отдельного callback-события для него нет.

## Логирование SDK

iOS SDK поддерживает параметр `logLevel` в `initialize(...)`. Он управляет только внутренними логами SDK и не влияет на `gravityEventCallback`, отправку событий или логи приложения.

```swift
GravitySDK.initialize(
    apiKey: "YOUR_API_KEY",
    section: "YOUR_SECTION_ID",
    gravityEventCallback: { event in
        print(event)
    },
    logLevel: .debug
)
```

Доступные уровни:

- `.debug` — включает HTTP request/response debug-логи и ошибки SDK;
- `.info` — включает `info` и `error` сообщения SDK;
- `.error` — оставляет только ошибки SDK;
- `.none` — полностью отключает внутренний вывод SDK.

Логи пишутся через `os_log` с префиксом `GravitySDK`.

Если нужно передавать внутренние логи SDK в приложение, используйте `setLogListener(...)`.

Пример:

```swift
GravitySDK.instance.setLogListener { level, message in
    print("[GravitySDK][\(level)] \(message)")
}
```

Обработчик получает только те сообщения, которые проходят текущий `logLevel`. Он вызывается на потоке, где сформирован лог; для обновления UI переключайтесь на главный поток.

`.none` отключает локальный вывод и log listener. Отправка внутренних ошибок в `sdk-sentry.gravityfield.ai/error` выполняется отдельно и не отключается этим уровнем. В `.debug` выводятся полные запросы и ответы, включая заголовок Authorization.

## Глобальные настройки

Метод задает runtime-настройки для последующих запросов `trackView(...)`, `triggerEvent(...)` и `getContentBySelector(...)`.

Пример:

```swift
GravitySDK.instance.setOptions(
    options: Options(
        isReturnCounter: false,
        isReturnUserInfo: true,
        isReturnAnalyticsMetadata: false,
        isImplicitPageview: false,
        isImplicitImpression: true
    ),
    contentSettings: ContentSettings(
        skusOnly: false,
        fields: ["id", "name", "price"]
    ),
    proxyUrl: nil
)
```

Что настраивается через `Options`:

- `isReturnCounter` — добавляет счетчики в response, если они нужны приложению;
- `isReturnUserInfo` — включает возврат `user` в response;
- `isReturnAnalyticsMetadata` — включает возврат аналитических метаданных;
- `isImplicitPageview` — управляет неявной отправкой pageview на backend;
- `isImplicitImpression` — управляет неявной отправкой impression для поддерживаемых сценариев.

Что настраивается через `ContentSettings`:

- `skusOnly` — возвращает только SKU вместо полного товарного объекта там, где это поддерживает backend-сценарий;
- `fields` — ограничивает набор полей, возвращаемых по товарам.

По умолчанию первые четыре флага `Options` равны `false`, `isImplicitImpression` — `true`. `isBuildEngagementUrl` всегда равен `true`; параметра для его изменения в публичном инициализаторе нет.

`setOptions(...)` заменяет настройки целиком: `nil` сбрасывает соответствующий объект к значениям по умолчанию. `proxyUrl` задаёт полный базовый URL API, например `https://proxy.example.com/v2`, без завершающего `/`. SDK добавляет к нему `/visit`, `/event` или `/choose`. Tracking URL из ответа и адрес отправки ошибок этим параметром не заменяются.

## `gravityEventCallback`

`gravityEventCallback` передается в `initialize(...)` опционально. Если приложению нужно обрабатывать действия пользователя внутри кампаний, передайте callback при инициализации SDK.

В SDK определены модели `TrackingEvent`, включая:

- `ContentLoadEvent`
- `ContentImpressionEvent`
- `ContentVisibleImpressionEvent`
- `ContentCloseEvent`
- `CopyEvent`
- `CancelEvent`
- `FollowUrlEvent`
- `FollowDeeplinkEvent`
- `RequestPushEvent`
- `ProductImpressionEvent`
- `ProductClickEvent`

SDK передает эти события в `gravityEventCallback` и выполняет вызов на главном потоке.

Практический вывод:

- `ContentLoadEvent` может приходить после `getContentBySelector(...)`, а также после внутренней загрузки контента для auto-rendered in-app кампаний;
- используйте `gravityEventCallback` для обработки URL, deeplink и других действий пользователя внутри кампаний;
- SDK сам не выполняет переходы по `FollowUrlEvent` и `FollowDeeplinkEvent`, это ответственность приложения;
- manual engagement через `sendContentEngagement(...)` / `sendProductEngagement(...)` не генерирует callback-событие автоматически;
- `ProductClickEvent` может приходить из встроенного rendering flow SDK, но не из ручного вызова `sendProductEngagement(...)`.

## Передача статуса Push-уведомлений

Если таргетинг кампаний зависит от статуса push-разрешения, передавайте его в SDK через `setNotificationPermissionStatus(status:)`.

```swift
GravitySDK.instance.setNotificationPermissionStatus(status: .granted)
```

Отдельно учитывайте поведение `requestPush` внутри in-app кампаний:

- iOS SDK сам вызывает `UNUserNotificationCenter.requestAuthorization(...)`;
- при отказе SDK может открыть системные настройки приложения;
- SDK дополнительно передает `RequestPushEvent` в `gravityEventCallback`.

## Блокировка показа in-app кампаний

Используйте presentation lock, когда приложение показывает собственную модалку, bottom sheet, onboarding, checkout или другой приоритетный слой интерфейса, который не должен перекрываться кампанией Gravity.

```swift
func showCheckoutSheet() {
    GravitySDK.instance.lockPresentation()

    let checkoutViewController = CheckoutViewController()
    checkoutViewController.onDismiss = {
        GravitySDK.instance.unlockPresentation()
    }

    present(checkoutViewController, animated: true)
}
```

Пока блокировка активна, SDK может загрузить контент после `trackView(...)` или `triggerEvent(...)`, но не будет показывать новую in-app кампанию поверх текущего интерфейса. Уже открытый Gravity-контент этим вызовом не закрывается.

Блокировка также применяется к переходам между шагами кампании через `openStep`. Ручная загрузка selector-контента через `getContentBySelector(...)` не блокируется: приложение продолжает само управлять показом manual rendering сценариев.

Если приложению нужно отслеживать состояние блокировки, подпишитесь на изменения:

```swift
GravitySDK.instance.setPresentationLockListener { locked in
    // Обновите состояние приложения, если это нужно вашему UI.
}
```

`lockPresentation()` и `unlockPresentation()` устанавливают один Boolean-флаг, счётчика вложенных блокировок нет. Если одновременно открываются несколько собственных слоёв UI, приложение должно само определить момент разблокировки. Пропущенная кампания не ставится в очередь и не показывается автоматически после `unlockPresentation()`.

Listener вызывается на главном потоке при вызове lock/unlock, но не получает текущее состояние сразу при подписке. Для снятия подписки передайте `nil`.
