iOS
Integration for iPhone and iPad apps. The shared flow is described under WebView: shared flow; this page has ready-to-use Swift code.
What you'll need
- A
WKWebViewwith anAdEventsHandlermessage handler. - A video player — the example uses
AVPlayer. - The SDK page address for the
iosplatform:https://cdn.adsdk.ru/ios/v3/.
Screen layout
How events arrive
Each event is a single object: name holds the event name, and the remaining fields are its data.
json
{ "name": "AdLoaded", "ad_media_file_src_url": "https://cdn.example.com/ad.mp4", "ad_creative_type": "video/mp4", "ad_number": 1 }See each event's fields under Events.
Full example
Create the WKWebView and player
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 holds the handler strongly — pass a proxy with a weak reference
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)")
}
}Receive events
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 plays inside the 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
}
}
}
/// Keeps WKUserContentController from retaining the controller after the ad closes.
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)
}
}Report playback to the SDK
swift
private extension AdViewController {
func observePlayer() {
// Position every 250 ms → quartiles and the skip point
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))")
}
// Start and pause
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
}
}
// End of the ad → 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))")
}
}
}Finish the ad
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?()
}
}Common issues
| Symptom | Cause |
|---|---|
| White screen instead of the ad | isOpaque = false and backgroundColor = .clear aren't set. |
| Video opens full-screen | allowsInlineMediaPlayback = true is missing. |
| VPAID doesn't start without a tap | mediaTypesRequiringUserActionForPlayback = [] is missing. |
| The controller isn't released | The handler was added directly, without a weak reference. |
| No quartiles | timeupdateAd() isn't being called. |
More in Troubleshooting.