iOS
Интеграция в приложения для iPhone и iPad. Общая схема — в разделе WebView: общая схема, здесь — готовый код на Swift.
Что понадобится
WKWebViewс обработчиком сообщенийAdEventsHandler.- Видеоплеер — в примере
AVPlayer. - Адрес страницы SDK с платформой
ios:https://cdn.adsdk.ru/ios/v3/.
Как устроен экран
Как приходят события
Каждое событие — один объект: name содержит имя события, остальные поля — данные.
json
{ "name": "AdLoaded", "ad_media_file_src_url": "https://cdn.example.com/ad.mp4", "ad_creative_type": "video/mp4", "ad_number": 1 }Поля каждого события — в разделе События.
Полный пример
Создайте WKWebView и плеер
swift
import AVFoundation
import UIKit
import WebKit
final class AdViewController: UIViewController {
private let player = AVPlayer()
private lazy var playerLayer = AVPlayerLayer(player: player)
private var webView: WKWebView!
private var timeObserver: Any?
private var statusObservation: NSKeyValueObservation?
private let vastURL: String
var onFinish: (() -> Void)?
init(vastURL: String) {
self.vastURL = vastURL
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
view.layer.addSublayer(playerLayer)
let config = WKWebViewConfiguration()
config.allowsInlineMediaPlayback = true
config.mediaTypesRequiringUserActionForPlayback = []
// WKUserContentController держит обработчик сильно — передаём прокси со слабой ссылкой
config.userContentController.add(WeakMessageHandler(self), name: "AdEventsHandler")
webView = WKWebView(frame: view.bounds, configuration: config)
webView.isOpaque = false
webView.backgroundColor = .clear
webView.scrollView.isScrollEnabled = false
webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(webView)
observePlayer()
webView.load(URLRequest(url: adPageURL()))
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
playerLayer.frame = view.bounds
}
private func adPageURL() -> URL {
var components = URLComponents(string: "https://cdn.adsdk.ru/ios/v3/")!
components.queryItems = [
URLQueryItem(name: "url", value: vastURL),
URLQueryItem(name: "lang", value: "ru"),
URLQueryItem(name: "controls", value: #"["ad-skip-btn","ad-click-btn","ad-more-menu"]"#),
]
return components.url!
}
private func sdk(_ call: String) {
webView.evaluateJavaScript("window.myAdController.\(call)")
}
}Получайте события
swift
extension AdViewController: WKScriptMessageHandler {
func userContentController(_ controller: WKUserContentController, didReceive message: WKScriptMessage) {
guard let body = message.body as? [String: Any], let name = body["name"] as? String else { return }
switch name {
case "AdLoaded":
if body["ad_creative_type"] as? String == "application/javascript" {
sdk("playAd()") // VPAID играет внутри WebView
} else if let src = body["ad_media_file_src_url"] as? String, let url = URL(string: src) {
player.replaceCurrentItem(with: AVPlayerItem(url: url))
player.play()
}
case "AdSkipped":
player.pause()
case "AdClick":
if let src = body["url"] as? String, let url = URL(string: src) {
UIApplication.shared.open(url)
}
case "AdError":
finish()
case "AdDestroyed":
player.pause()
if body["ad_is_last"] as? Bool == true { finish() }
default:
break
}
}
}
/// Не даёт WKUserContentController удерживать контроллер после закрытия рекламы.
final class WeakMessageHandler: NSObject, WKScriptMessageHandler {
private weak var target: WKScriptMessageHandler?
init(_ target: WKScriptMessageHandler) { self.target = target }
func userContentController(_ controller: WKUserContentController, didReceive message: WKScriptMessage) {
target?.userContentController(controller, didReceive: message)
}
}Сообщайте SDK о воспроизведении
swift
private extension AdViewController {
func observePlayer() {
// Позиция каждые 250 мс → квартили и момент пропуска
let interval = CMTime(seconds: 0.25, preferredTimescale: 600)
timeObserver = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] time in
guard let self, let duration = self.player.currentItem?.duration.seconds,
duration.isFinite, duration > 0 else { return }
self.sdk("timeupdateAd(\(time.seconds), \(duration))")
}
// Старт и пауза
statusObservation = player.observe(\.timeControlStatus) { [weak self] player, _ in
switch player.timeControlStatus {
case .playing: self?.sdk("playAd()")
case .paused where player.currentItem != nil: self?.sdk("pauseAd()")
default: break
}
}
// Конец ролика → AdComplete
NotificationCenter.default.addObserver(
forName: .AVPlayerItemDidPlayToEndTime, object: nil, queue: .main
) { [weak self] _ in
guard let self, let duration = self.player.currentItem?.duration.seconds else { return }
self.sdk("timeupdateAd(\(duration), \(duration))")
}
}
}Завершите показ
swift
private extension AdViewController {
func finish() {
if let timeObserver { player.removeTimeObserver(timeObserver) }
statusObservation = nil
player.replaceCurrentItem(with: nil)
webView.configuration.userContentController.removeScriptMessageHandler(forName: "AdEventsHandler")
onFinish?()
}
}Частые ошибки
| Симптом | Причина |
|---|---|
| Белый экран вместо ролика | Не выставлены isOpaque = false и backgroundColor = .clear. |
| Видео открывается на весь экран | Нет allowsInlineMediaPlayback = true. |
| VPAID не стартует без касания | Нет mediaTypesRequiringUserActionForPlayback = []. |
| Контроллер не освобождается | Обработчик добавлен напрямую, без слабой ссылки. |
| Нет квартилей | Не вызывается timeupdateAd(). |
Больше — в разделе Решение проблем.