<!-- Canonical: https://docs.shopstory.live/docs/reference/mobile-sdk-ios -->

# iOS SDK reference

Версия: `ShopStoryPlayerSDK 0.2.0-preview.2`. Описание сверено с исходниками immutable tag. Установка и собираемый starter: [native iOS](<https://docs.shopstory.live/docs/guide/mobile-sdk-ios>). Совместимость и приёмка: [версии и выпуск](<https://docs.shopstory.live/docs/guide/mobile-sdk-releases>).

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

```swift
try ShopStorySDK.shared.configure(
    applicationId: applicationId,
    hostAppVersion: hostAppVersion,
    isEnabled: true,
    followMePlayerEnabled: true,
    manualMinimizeEnabled: true
)
```

| Параметр | Тип / default | Назначение |
| --- | --- | --- |
| `applicationId` | `String`, обязательный | Выданный проект. SDK обрезает пробелы и приводит значение к нижнему регистру |
| `hostAppVersion` | `String? = nil` | Версия приложения для диагностики |
| `isEnabled` | `Bool = true` | Локальное разрешение SDK UI; remote policy может дополнительно его запретить |
| `followMePlayerEnabled` | `Bool = true` | Локальное разрешение Follow Me |
| `manualMinimizeEnabled` | `Bool = true` | Разрешение ручного скрытия floating player к краю |

Некорректный `applicationId` вызывает `ShopStorySDKConfigurationError.invalidApplicationId` и отключает SDK surfaces без завершения host app. Успешный `configure` начинает cache-aware обновление policy; он не означает готовность видео. Обновление policy, её cache и транспорт принадлежат SDK.

## Product lookup

```swift
let request: ShopStoryProductStreamStateRequest =
    ShopStorySDK.shared.loadProductStreamState(
        feedProductId: feedProductId
    ) { result in
        // Result<ShopStoryProductStreamState, ShopStoryProductStreamStateError>
        handleProductState(result)
    }

// При уходе с PDP или замене товара:
request.cancel()
```

`handleProductState` — функция host app. Completion доставляется асинхронно на main actor. Храните request для текущего товара. `cancel()` отменяет незавершённый запрос и подавляет его completion; ожидать callback отмены не нужно.

| Результат | Действие host app |
| --- | --- |
| `.success(.active(entrypoint))` | Показать SDK entrypoint для текущего товара |
| `.success(.unavailable(reason))` | Убрать SDK-блок без пустого placeholder |
| `.failure(error)` | Убрать SDK-блок, сохранить PDP рабочей и записать typed error в диагностику |

`ShopStoryProductStreamUnavailableReason` содержит `noActiveStream`, `scheduled`, `archived`, `disabled`, `notEligible`. Это варианты типа SDK. Опубликованный HTTP-контракт сейчас возвращает `no_active_stream` для недоступного видео; наличие остальных enum cases не обещает их доступность в Public API.

### Entrypoint

`ShopStoryProductDetailEntrypoint` возвращается SDK; приложение не собирает его из URL.

| Поле | Тип | Назначение |
| --- | --- | --- |
| `streamId` | `String` | Идентификатор эфира или записи |
| `feedProductId` | `String` | Товарный ID клиента, с сохранением ведущих нулей |
| `productId` | `String?` | Внутренний ID ShopStory для диагностики |
| `previewURL`, `posterURL` | `URL?` | Медиа preview и poster, если доступны |
| `startTime` | `Int?` | Стартовая позиция из состояния товара |
| `mediaOrientation` | `ShopStoryMediaOrientation` | `portrait`, `landscape`, `square`, `unknown` |

Адрес player внутри entrypoint не является публичным полем. Передавайте объект в `coordinator.load(entrypoint:)` целиком.

### Ошибки lookup

| `ShopStoryProductStreamStateError` | Проверка |
| --- | --- |
| `sdkNotConfigured` | `configure` успешно выполнен до lookup |
| `runtimePolicyDisabled` | Проект и текущая runtime policy разрешают SDK UI |
| `invalidProductIdentifier` | Передан непустой строковый `feedProductId` из фида |
| `networkFailure` | Доступна сеть; повтор разрешён только пока товар остаётся текущим |
| `invalidResponse`, `decodingFailed` | Версия SDK соответствует контракту; приложите очищенную диагностику в поддержку |
| `requestSuperseded` | Конфигурация SDK изменилась до завершения lookup; используйте актуальный запрос |

Не заменяйте техническую ошибку утверждением «у товара нет видео»: для этого есть успешный `.unavailable`.

## Player coordinator

`ShopStoryPlayerCoordinator` и его delegate работают на `@MainActor`. Integration root удерживает один coordinator на активную playback session. `delegate` — weak; host app удерживает его отдельно.

| API | Поведение |
| --- | --- |
| `init(delegate:)` | Создаёт coordinator; delegate необязателен |
| `playerView: UIView` | Opaque native view. Внутренний WebView не передаётся host app |
| `attach(to: UIView)` | Монтирует player view в контейнер с constraints по его границам |
| `load(entrypoint:)` | Загружает разрешённый товарный entrypoint |
| `load(trustedPlayerURL:)` | Для native списка эфиров: только HTTPS URL, полученный напрямую из ShopStory API |
| `detach()` | Снимает view с контейнера без завершения playback session |
| `pausePlayback(reason:)` | Приостанавливает media; причины: `appBackgrounded`, `hostRequested`, `surfaceBlocked`, `teardown` |
| `resumePlayback()` | Возобновляет media с учётом текущего состояния coordinator |
| `teardown()` | Завершает session и освобождает player. Для нового воспроизведения создайте новый coordinator |

При full-screen → PDP → Follow Me переносите тот же `playerView` через `detach` / `attach`. Повторный `load` не нужен. Полный пример owner: [Full-screen player и Follow Me](<https://docs.shopstory.live/docs/guide/mobile-sdk-ios#full-screen-player-%D0%B8-follow-me>).

## Product actions

Delegate получает `ShopStoryProductAction` через `shopStoryPlayerCoordinator(_:didReceive:)`. `kind` — `openProduct` или `addProduct`. `payload` содержит необязательные `name`, `feedProductId`, `feedProductGroupId`, `url`, `vendorCode`, `source`.

Проверьте `payload.feedProductId` и разрешите его через native catalogue. `url` — справочное поле, оно не должно управлять native navigation. В delegate нет completion: действие передаётся в одну сторону. Приложение самостоятельно открывает PDP или выполняет Cart API и показывает подтверждённый результат в своём UI.

## Lifecycle diagnostics

Delegate получает `ShopStoryPlayerLifecycleEvent` через `shopStoryPlayerCoordinator(_:didUpdate:)`.

| Event | Смысл |
| --- | --- |
| `pageLoaded` | Завершилась навигация страницы player. Это не подтверждение первого video frame |
| `closeRequested` | Player запросил закрытие; host завершает session |
| `backgroundPaused`, `foregroundRestored` | Изменился app lifecycle; перед показом Follow Me снова проверьте surface и policy |
| `navigationFailed(String)` | Ошибка загрузки; строка предназначена для очищенной диагностики |
| `navigationBlocked(String)`, `bridgeMessageBlocked(String)` | SDK отклонил navigation или bridge message; не обходите проверку |
| `runtimePolicyDisabled` | SDK UI отключён runtime policy; освободите host container |
| `productActionDecodeFailed(kind)` | Product action не удалось разобрать; не выполняйте commerce-действие |

Состояния coordinator не заменяют [commerce events](<https://docs.shopstory.live/docs/reference/events-metrics>). Публичного native события «оплаченный заказ» в этом контракте нет.

## Surface и floating player

После изменения видимого экрана вызывайте `setCurrentSurface(_:)`. Доступные стандартные значения: `home`, `liveList`, `catalog`, `search`, `productDetail`, `cart`, `checkout`, `payment`, `auth`, `profile`, `unknown`. Для дополнительной поверхности используйте `ShopStorySurface(rawValue:)` и согласованную policy. Неизвестный экран не следует считать разрешённым.

`shouldOpenPlayer()` проверяет разрешение player UI. `shouldShowFollowMePlayer(on:)` проверяет заданную или текущую поверхность. Геометрию возвращают `followMeGeometry(for:)` и overloads с `mediaOrientation`.

После `configure` создайте и удерживайте `makeFloatingPlayerController()`. Его `show(using:)` применяет геометрию; `hide(to:)` скрывает к краю; `restore()` восстанавливает; `suppress()` убирает UI по policy; `close()` закрывает floating UI. Закрытие controller не заменяет `coordinator.teardown()` при завершении session.

SwiftUI host может использовать `ShopStoryProductDetailPreview`, `ShopStoryProductDetailEntrypointView` и `ShopStoryManagedFloatingPlayerContainer`. Containers и route owner остаются частью приложения; ориентируйтесь на sample выбранной версии.

## Обновление с preview.1

В товарной интеграции замените старый `load(playerURL:)` передачей entrypoint целиком:

```swift
coordinator.load(entrypoint: entrypoint)
```

Для списка эфиров старый `load(playerURL:)` заменён на `load(trustedPlayerURL:)`. Не передавайте в него URL из product action или пользовательского ввода. Затем обновите lockfile и повторите [приёмку](<https://docs.shopstory.live/docs/guide/mobile-sdk-releases#%D0%BF%D1%80%D0%B8%D1%91%D0%BC%D0%BA%D0%B0-%D0%BF%D0%B5%D1%80%D0%B5%D0%B4-%D0%B2%D1%8B%D0%BF%D1%83%D1%81%D0%BA%D0%BE%D0%BC>).

Разбор проблем: [диагностика Mobile SDK](<https://docs.shopstory.live/docs/operations/troubleshooting#mobile>). [Flutter reference](<https://docs.shopstory.live/docs/reference/mobile-sdk-flutter>) описывает отдельный adapter contract.
