Skip to content

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 WKWebView with an AdEventsHandler message handler.
  • A video player — the example uses AVPlayer.
  • The SDK page address for the ios platform: 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

SymptomCause
White screen instead of the adisOpaque = false and backgroundColor = .clear aren't set.
Video opens full-screenallowsInlineMediaPlayback = true is missing.
VPAID doesn't start without a tapmediaTypesRequiringUserActionForPlayback = [] is missing.
The controller isn't releasedThe handler was added directly, without a weak reference.
No quartilestimeupdateAd() isn't being called.

More in Troubleshooting.