Интеграция iOS SDK
Подключите Melvad к iOS-приложению, настройте рекламные места и управляйте загрузкой и показом из Swift. Ниже — общий цикл интеграции и примеры для каждого поддерживаемого формата.
Инструкция для Melvad iOS SDK 1.0.2. Подключите бинарный пакет через Swift Package Manager или скачайте готовую поставку по ссылке ниже.
Другие интеграции: Синхронизация кук Форматы OpenRTB
1. Подготовьте приложение
- Зарегистрируйтесь в Melvad и создайте инвентарь типа «Приложение» для iOS. В карточке приложения укажите его Bundle ID и ссылку на настоящую страницу в App Store.
- Создайте рекламные места для нужных форматов. Получите
inventoryIDиplacementIDиз настроек или тега размещения. Для разных форматов используйте соответствующие им рекламные места. - Получите у команды Melvad числовой
clientID. Production-адресhttps://pub.supplier.melvad.comуже задан в SDK по умолчанию. SDK подключается из GitHub на следующем шаге; для тестовой среды адрес можно переопределить.
Требуются iOS 16+ и Xcode с поддержкой Swift 6. Примеры используют UIKit. В SwiftUI размещайте рекламный UIView через UIViewRepresentable и удерживайте владельца сессии в coordinator или модели экрана.
Bundle ID и App Store URL в конфигурации SDK должны соответствовать приложению, указанному в инвентаре. Данные приложения для подбора рекламы задаются в настройках инвентаря.
2. Подключите SDK
- В Xcode откройте File → Add Package Dependencies.
- Введите адрес пакета: https://github.com/Melvad/ios-ads-sdk.
- Выберите Exact Version: 1.0.2 и добавьте в target приложения оба продукта:
MelvadAdsCoreиMelvadAdsRuntime. - Swift Package Manager загрузит XCFramework для устройства и симулятора вместе с ресурсами. Prebid уже включён в поставку; отдельно вторую копию подключать не нужно.
Для локального подключения клонируйте репозиторий SDK и выберите его корневой каталог с Package.swift через Add Local…. Готовые XCFramework находятся в папке Frameworks. Сохраняйте структуру каталогов, ресурсы и лицензии.
Для подключения без загрузки пакета из Git скачайте MelvadAdsSDK 1.0.2, распакуйте архив и добавьте каталог с Package.swift через Add Local…. Сохраняйте все бинарные файлы и resource bundles вместе. Контрольные суммы SHA-256 приложены к релизу.
MelvadAdsTracking — опциональный продукт. Добавляйте его, если приложению нужны рекламные идентификаторы при разрешённом ATT; настройка описана в разделе privacy.
Условия использования: лицензия SDK.
3. Настройте запросы
Создайте конфигурацию один раз для каждого маршрута: .display для баннеров и native, .video для видео и rewarded. Передайте в функцию выданные Melvad значения и App Store URL вашего приложения.
По умолчанию используется production-адрес https://pub.supplier.melvad.com; аргумент supplierURL можно не передавать. Для тестовой среды задайте его явно. При прямом создании SupplierConfiguration такое же значение по умолчанию применяется к baseURL.
import UIKit
import WebKit
import MelvadAdsCore
import MelvadAdsRuntime
enum IntegrationError: Error {
case missingBundleID, missingUserAgent
}
@MainActor
func makeConfiguration(
supplierURL: URL = SupplierConfiguration.productionBaseURL,
clientID: Int64,
appStoreURL: URL,
route: SupplierConfiguration.Route,
identityProvider: (any AdvertisingIdentityProvider)? = nil
) async throws -> SupplierConfiguration {
guard let bundleID = Bundle.main.bundleIdentifier else {
throw IntegrationError.missingBundleID
}
let webView = WKWebView(frame: .zero)
guard let userAgent = try await webView.evaluateJavaScript(
"navigator.userAgent"
) as? String, !userAgent.isEmpty else {
throw IntegrationError.missingUserAgent
}
return try SupplierConfiguration(
baseURL: supplierURL,
clientID: clientID,
bundleID: bundleID,
storeURL: appStoreURL,
userAgent: userAgent,
language: Locale.preferredLanguages.first,
route: route,
identityProvider: identityProvider
)
}
supplierURL — базовый адрес без /get, /video, query-параметров и идентификатора клиента. SDK сам добавляет /get/{clientID} или /video/{clientID}. Используйте HTTPS; отключать ATS для этой интеграции не требуется.
Далее displayConfiguration и videoConfiguration — результаты этой функции с соответствующим route. Вызывайте её через try await из @MainActor и обрабатывайте ошибку настройки.
4. Передайте согласия пользователя
Приложение управляет CMP и запросом ATT; SDK не показывает системный диалог. Создайте PrivacyContext по фактическим настройкам пользователя, сохраняя неизвестные значения неизвестными.
По умолчанию MelvadAdsCore и MelvadAdsRuntime не читают IDFA/IDFV. Для запросов SDK приложению не требуется подключать AdSupport или AppTrackingTransparency.
import MelvadAdsCore
@MainActor
func makePrivacy(
gdprApplies: Bool?,
consentString: String?,
personalizationAllowed: Bool,
globalPrivacyControl: Bool
) -> PrivacyContext {
PrivacyContext(
personalizationAllowed: personalizationAllowed,
gdprApplies: gdprApplies,
consentString: consentString,
globalPrivacyControl: globalPrivacyControl
)
}
Если нужен сбор рекламных идентификаторов, добавьте продукт MelvadAdsTracking и передайте SystemAdvertisingIdentityProvider() при создании конфигурации:
import MelvadAdsTracking
// Inside a @MainActor method, after your app has handled consent:
let configuration = try await makeConfiguration(
clientID: clientID,
appStoreURL: appStoreURL,
route: .display,
identityProvider: SystemAdvertisingIdentityProvider()
)
Provider перечитывает ATT перед каждым запросом. Идентификаторы доступны только при ATT authorized, personalizationAllowed == true, явно заданном gdprApplies == false и globalPrivacyControl == false. При denied, restricted и notDetermined не отправляется ни IDFA, ни IDFV. IDFV не используется для обхода отказа в ATT.
Если приложение запрашивает ATT, задайте NSUserTrackingUsageDescription и используйте API Apple. Сохраняйте privacy manifests SDK в сборке и проверяйте Privacy Report приложения с учётом подключённых партнёров спроса.
Отсутствие обоих идентификаторов не блокирует запрос. После изменения согласий вызовите updatePrivacy: текущая загрузка отменится, подготовленный креатив будет удалён. Новую загрузку запускает приложение.
5. Создайте владельца рекламного места
Общий порядок: load → ready → show. Для fullscreen вместо show() можно использовать present(from:). Загрузка сама по себе не означает показ. Все вызовы и callbacks выполняются на MainActor.
Ниже — небольшой класс приложения поверх реального API SDK. Он удерживает сессию, обрабатывает клики и вызывает onReady только после загрузки: обычный hide() не запускает показ повторно.
Полный код AdSlotController.swift
import UIKit
import MelvadAdsCore
import MelvadAdsRuntime
@MainActor
final class AdSlotController: NSObject, MelvadAdSessionDelegate {
let session: MelvadAdSession
var onReady: ((MelvadAdSession) -> Void)?
var onUnavailable: (() -> Void)?
var onError: ((NSError) -> Void)?
var onEvent: ((String) -> Void)?
private var awaitingLoad = false
init(configuration: SupplierConfiguration, privacy: PrivacyContext) {
session = MelvadAdSession(configuration: configuration)
super.init()
session.updatePrivacy(privacy)
session.delegate = self
}
func load(in slot: UIView, inventoryID: String,
placementID: String, mode: String? = nil) {
slot.isHidden = false
slot.superview?.layoutIfNeeded()
guard slot.window != nil,
slot.bounds.width >= 1, slot.bounds.height >= 1 else {
onError?(NSError(domain: "MelvadIntegration", code: 1,
userInfo: [NSLocalizedDescriptionKey:
"Attach the ad slot to a window and set its size before loading."]))
return
}
awaitingLoad = true
session.load(in: slot, inventoryID: inventoryID,
placementID: placementID, mode: mode,
width: Int(slot.bounds.width),
height: Int(slot.bounds.height))
}
func adSession(_ session: MelvadAdSession,
didChange state: MelvadAdSessionState) {
switch state {
case .ready where awaitingLoad:
awaitingLoad = false
onReady?(session)
case .noFill, .destroyed:
awaitingLoad = false
onUnavailable?()
case .idle, .failed:
awaitingLoad = false
default:
break
}
}
func adSession(_ session: MelvadAdSession, didFail error: NSError) {
onError?(error)
}
func adSession(_ session: MelvadAdSession, didReceiveEvent event: String) {
onEvent?(event)
}
func adSession(_ session: MelvadAdSession, didClick url: URL) {
UIApplication.shared.open(url)
}
}Сохраните этот класс в проекте. В своём UIViewController удерживайте владельцев рекламных мест как свойства: SDK хранит delegate слабо. Один объект обслуживает одно размещение.
// Properties of your UIViewController, not local variables:
private var displayAd: AdSlotController?
private var fullscreenAd: AdSlotController?
private var videoAd: AdSlotController?
В следующих примерах privacy — ваш PrivacyContext, идентификаторы получены из Melvad, а slot — UIView с заданными размерами, уже добавленный в окно. Запускайте загрузку после layout, например однократно в viewDidAppear. Не вызывайте её на каждом viewDidLayoutSubviews.
Поддерживаемые форматы
| Формат | Маршрут | Mode | Показ |
|---|---|---|---|
| Баннер / MREC | .display | nil | show() |
| Полноэкранный баннер | .display | "fullscreen" | present(from:) |
| Закреплённый / раскрывающийся баннер | .display | nil | show() |
| Native image / native video | .display | nil | show() |
| Inread / infeed / inslide | .video | "inread" / "infeed" / "inslide" | show() |
| Полноэкранное видео | .video | "fullscreen" | show() / present(from:) |
| Rewarded | .video | "rewarded" | present(from:) |
| Pre- / mid- / post-roll | .video | nil | MelvadAdBreakController |
Шаблон и креатив выбирает сервер по настройкам рекламного места. mode передаёт режим показа и не превращает обычное баннерное размещение в native или video. Размеры в примерах — в UIKit points, а не физических пикселях.
7. Полноэкранный баннер
Создайте display-размещение с полноэкранным шаблоном. Подготовьте объявление в UIView текущего экрана; его ширина и высота должны быть положительными. После .ready показывайте рекламу в подходящей точке перехода интерфейса.
// Prepare a fullscreen banner using displayConfiguration:
fullscreenAd?.session.destroy()
let ad = AdSlotController(configuration: displayConfiguration, privacy: privacy)
fullscreenAd = ad
ad.onError = { error in
print(error.domain, error.code, error.localizedDescription)
}
ad.load(in: slot, inventoryID: inventoryID,
placementID: placementID, mode: "fullscreen")// A method of your UIViewController; call at an appropriate UI transition:
func showPreparedFullscreen() {
guard let session = fullscreenAd?.session,
session.state == .ready,
view.window != nil,
presentedViewController == nil else { return }
do {
try session.present(from: self)
} catch {
print(error.localizedDescription)
}
}
Показывайте из той же scene, в которой находится исходный контейнер. Одновременно в scene доступен один fullscreen-показ. Закрытие пользователем уничтожает сессию; для следующего объявления создайте новую.
8. Закреплённый баннер и раскрытие
Для adhesion и adhesion fullscreen используйте .display и предназначенное для них рекламное место. Отдельное значение mode: "adhesion" не требуется. SDK размещает баннер у нижней safe area; приложение оставляет под ним отступ для контента.
// Use a placement configured for adhesion or expandable adhesion.
displayAd?.session.destroy()
let ad = AdSlotController(configuration: displayConfiguration, privacy: privacy)
displayAd = ad
ad.onReady = { $0.show() }
ad.onUnavailable = { [weak slot] in slot?.isHidden = true }
ad.onError = { [weak slot] error in
slot?.isHidden = true
print(error.localizedDescription)
}
ad.load(in: slot, inventoryID: inventoryID, placementID: adhesionPlacementID)
Раскрытие на весь экран доступно, если его разрешают настройки supplier и MRAID-креатив. Оно запускается действием пользователя. Закрытие раскрытого объявления возвращает тот же баннер; закрытие самого баннера завершает сессию.
Поддерживаются команды MRAID для открытия ссылок, видимости, геометрии, expand, close и playVideo. В текущем профиле resize() означает раскрытие на весь экран; two-part expansion и принудительная ориентация не поддерживаются.
9. Native image и native video
Оба формата используют displayConfiguration. Выберите рекламное место для native image или native video. SDK сам строит карточку из ответа; вручную запрашивать и собирать assets не нужно.
// Use the placement ID for native image OR native video.
displayAd?.session.destroy()
let ad = AdSlotController(configuration: displayConfiguration, privacy: privacy)
displayAd = ad
ad.onReady = { $0.show() }
ad.onUnavailable = { [weak nativeSlot] in nativeSlot?.isHidden = true }
ad.onError = { [weak nativeSlot] error in
nativeSlot?.isHidden = true
print(error.localizedDescription)
}
ad.load(in: nativeSlot, inventoryID: inventoryID, placementID: nativePlacementID)
Размещайте nativeSlot внутри ленты или статьи, с обычным контентом до и после него. Для карточки задайте достаточную высоту; в примере SDK используется 460 pt. Native показывается через show(); present(from:) для него не поддерживается.
Native video начинает воспроизведение без звука после разрешения показа и при видимости не менее 50%. За пределами экрана и в фоне оно приостанавливается. После завершения уберите слот. Текущий native-video путь поддерживает один VAST InLine с линейным MP4-креативом; цепочки VAST wrappers для него не заявлены.
10. Inread, infeed, inslide и fullscreen video
Используйте videoConfiguration и видеорекламное место. Метод ниже загружает объявление и разрешает показ после готовности. Выберите один из четырёх режимов.
| Mode | Поведение |
|---|---|
.inread | В статье; воспроизведение при видимости от 50%. |
.infeed | В ленте; старт после 1 секунды непрерывной видимости от 50%, пауза за экраном. |
.inslide | Выезжающий блок: снизу на iPhone, справа на iPad. |
.fullscreen | Полноэкранный видеопоказ с элементами управления SDK. |
// A method of your UIViewController:
func loadVideo(in slot: UIView, configuration: SupplierConfiguration,
privacy: PrivacyContext, inventoryID: String,
placementID: String, mode: MelvadVideoMode) {
videoAd?.session.destroy()
let ad = AdSlotController(configuration: configuration, privacy: privacy)
videoAd = ad
ad.onReady = { $0.show() }
ad.onUnavailable = { [weak slot] in slot?.isHidden = true }
ad.onError = { [weak slot] error in
slot?.isHidden = true
print(error.localizedDescription)
}
ad.load(in: slot, inventoryID: inventoryID, placementID: placementID,
mode: mode.supplierValue)
}
// Call from your screen after layout; choose one mode for this placement:
// loadVideo(in: slot, configuration: videoConfiguration, privacy: privacy,
// inventoryID: inventoryID, placementID: placementID, mode: .inread)
// Other modes: .infeed, .inslide, .fullscreen
Для inread/infeed подготовьте слот внутри статьи или ленты, обычно с пропорциями 16:9. Для inslide и fullscreen исходный UIView также должен находиться в окне. При fullscreen на экране не должно быть другой открытой модали. Если нужно разделить подготовку и момент показа, не вызывайте show() в onReady: вызовите его позже из состояния .ready.
11. Видео за вознаграждение
Используйте videoConfiguration, отдельное rewarded-размещение и строку mode: "rewarded". Значение rewarded не входит в enum MelvadVideoMode. Пользователь должен выбрать просмотр до запуска показа.
// Call after the user opts in to watch a rewarded ad:
fullscreenAd?.session.destroy()
let ad = AdSlotController(configuration: videoConfiguration, privacy: privacy)
fullscreenAd = ad
ad.onReady = { [weak self] _ in self?.showPreparedFullscreen() }
ad.onUnavailable = { print("Rewarded ad unavailable or closed") }
ad.onError = { error in print(error.localizedDescription) }
ad.onEvent = { event in
if event == "AdVideoComplete" {
print("Video completed; this is not a verified reward")
}
}
ad.load(in: slot, inventoryID: inventoryID,
placementID: rewardedPlacementID, mode: "rewarded")
Подтверждение награды. AdVideoComplete сообщает только о завершении видео. В текущей поставке нет готового контракта SSV для серверной проверки наград. Не начисляйте подтверждённую награду по этому callback; до запуска монетизации с наградами согласуйте серверный контракт с Melvad.
12. Pre-roll, mid-roll и post-roll
Внешний плеер воспроизводит основной контент, а MelvadAdBreakController ставит его на паузу и показывает рекламу. Для AVPlayer добавьте адаптер; для другого плеера реализуйте те же два метода.
import AVFoundation
import MelvadAdsRuntime
@MainActor
final class ContentAdapter: MelvadContentPlayback {
let player: AVPlayer
init(player: AVPlayer) { self.player = player }
func playContent() { player.play() }
func pauseContent() { player.pause() }
}
Добавьте рекламный UIView поверх области контентного плеера. После layout создайте контроллер с videoConfiguration и instream-размещением. Пример показывает pre-roll, один mid-roll на 30-й секунде и post-roll.
// Properties of your UIViewController:
private var contentAdapter: ContentAdapter?
private var adBreaks: MelvadAdBreakController?
// Inside a method, after laying out the ad overlay inside the content player:
adBreaks?.destroy()
let adapter = ContentAdapter(player: contentPlayer)
contentAdapter = adapter
let breaks = MelvadAdBreakController(
content: adapter,
container: adOverlay,
configuration: videoConfiguration,
inventoryID: inventoryID,
placementID: instreamPlacementID,
midrollTime: 30,
privacy: privacy
)
adBreaks = breaks
breaks.onEvent = { print($0) }
breaks.start()
Подключите периодический callback времени вашего плеера и событие окончания текущего контентного элемента. У AVPlayer это addPeriodicTimeObserver и AVPlayerItem.didPlayToEndTimeNotification. Передавайте события на MainActor и снимайте observers при освобождении экрана.
// Forward player callbacks on MainActor:
adBreaks?.contentTimeDidChange(contentPlayer.currentTime().seconds)
// Call the line above on periodic playback updates, not just once.
// When the current content item finishes:
adBreaks?.contentDidFinish()
// When the user's privacy settings change:
adBreaks?.updatePrivacy(updatedPrivacy)
// Before leaving the screen; also remove the player's time/end observers:
adBreaks?.destroy()
adBreaks = nil
contentAdapter = nil
Pre-/mid-/post-roll — точки вставки, а не значения mode. Для каждой паузы контроллер выполняет отдельный video-запрос с указанным placement. No-fill или ошибка возвращают управление контенту; после post-roll контент не запускается заново. Для нового видео или повторного воспроизведения создайте новый контроллер.
13. Жизненный цикл и проверка интеграции
// Pause and resume the SAME inline creative:
displayAd?.session.hide()
// Later, when the slot should be visible again:
displayAd?.session.show()
// After a privacy change, discard the prepared creative:
displayAd?.session.updatePrivacy(updatedPrivacy)
// Start a new load explicitly with the new privacy context.
// Before removing a slot, reusing a cell, or leaving the screen:
displayAd?.session.destroy()
displayAd = nil
videoAd?.session.destroy()
videoAd = nil
fullscreenAd?.session.destroy()
fullscreenAd = nil
destroy()— окончательная операция. После неё создайте новую сессию. Новыйloadв живой сессии заменяет предыдущую загрузку и объявление.- Готовый креатив хранится не более 120 секунд; Prebid может задать меньший срок. Если он истёк, загрузите новое объявление.
- Повторный
show()после inlinehide()использует тот же креатив. Закрытие модального видео уничтожает его сессию. - SDK управляет impression, billing и VAST trackers. Не отправляйте их повторно из приложения.
- На физическом устройстве проверьте получение каждого формата, клики, закрытие, возврат из фона, прокрутку, повторное использование ячеек и изменение согласий.
| Результат | Что проверить |
|---|---|
noFill | Штатное отсутствие объявления. Проверьте активность и формат placement, настройки спроса; уберите пустое место. |
failed | Сохраните domain/code из NSError. Проверьте HTTPS-адрес, clientID, inventory/placement, сеть и конфигурацию приложения. |
| Есть ready, но нет показа | Вызван ли show/present? Контейнер в окне, имеет размер и виден? Приложение в foreground? |
| Fullscreen не открывается | Та же scene, нет другой модали или полноэкранной рекламы, креатив не истёк. Для native нужен inline-показ. |
Автоматического повторного аукциона нет. Если нужен повтор, планируйте его с ограничением частоты; не запускайте бесконечный load() из callback ошибки. Проверка примеров с тестовыми ответами не гарантирует наличие рекламного спроса в конкретном размещении.
Для помощи напишите на support@melvad.com: укажите версию SDK, iOS, формат, inventory/placement и шаги воспроизведения. Перед отправкой логов удалите токены, идентификаторы устройства и данные согласий.
14. Нативный Open Measurement
Версия 1.0.2 включает нативный OMSDK_Melvad 1.6.12. Runtime управляет сессией измерения и событиями объявления/видео для display и VAST, включая pre-roll, mid-roll и post-roll. Дополнительная инициализация OMID в приложении не нужна.
Не подключайте второй web OMID Service/Session Client и не отправляйте события измерения того же креатива вручную. Ресурсы проверки VAST берутся из AdVerifications; без подходящих ресурсов видео продолжает воспроизведение без OMID-сессии. Наличие интеграции не означает сертификацию IAB.
15. Логирование SDK и ошибки
Логирование по умолчанию выключено. Выберите минимальный уровень debug, info, warning или error и задайте обработчик:
import MelvadAdsCore
MelvadLogger.configure(minimumLevel: .debug) { entry in
print("[\(entry.subsystem)] \(entry.message)")
}
// Disable SDK logging:
MelvadLogger.configure()
Swift-обработчик выполняется в потоке, который создал запись; работу с UI переносите на MainActor. Для Objective-C доступен MelvadDiagnostics.configure(minimumLevel:handler:) с callback на MainActor. SDK записывает состояния и статусы, без creative markup, значений consent и идентификаторов устройства.
В adSession(_:didFail:) проверяйте NSError.localizedDescription, domain, code и NSUnderlyingErrorKey. Исходная сетевая ошибка сохраняется. noFill остаётся штатным результатом, а не ошибкой.