Metrike Ads SDK — iOS
Guia de integração para publishers. Cobre o framework único MetrikeAds, os
seis formatos de anúncio, o lado do publisher e do advertiser da conversão
in-app, e as limitações declaradas do V1.
Status: V1 demo-grade com API production-shaped (nomes finais, interop
Objective-C, crash-safety, semver). Ver §11 do design spec
(docs/superpowers/specs/2026-09-02-ios-sdk-design.md) para o corte completo
V1 × V1.1. Este SDK é o porte iOS do SDK Android (item 9, shipped
2026-09-01) — o mesmo contrato de servidor (identidade, beacons, VAST/VMAP,
mk_cid), reescrito a partir do protocolo, não traduzido linha a linha do
Kotlin.
Downloads e links
O zip acima é o artefato binário publicado no cdx — na prática você não
baixa este arquivo manualmente: o SPM resolve e faz o download sozinho a
partir da URL declarada no Package.swift do repositório público (§1). O
link existe para quem precisa auditar o binário ou montar uma integração
manual fora do SPM.
Código-fonte de exemplo:
Diferente do Android, não existe um APK/build assinado para instalar
direto no aparelho — não há download público de app fora da App
Store/TestFlight nesse ecossistema. O deliverable de "app de exemplo" aqui é
o próprio código-fonte: clone o repositório acima e rode Example/ no Xcode
(inclusive no Simulator) — ver §11.
1. Instalação
Distribuído via Swift Package Manager (SPM) — nada de CocoaPods/Carthage
no V1. Repo público github.com/metrikehub/metrike-ads-ios — contém só
Package.swift, o app de exemplo e o README. O framework em si é fonte
fechado (decisão de negócio: não expor os algoritmos de medição): o
Package.swift declara um binaryTarget que resolve o .xcframework
zipado publicado no cdx, com checksum.
No Xcode: File → Add Package Dependencies → cole a URL do repo. Uma linha, nenhum arquivo pra editar:
https://github.com/metrikehub/metrike-ads-ios
O Package.swift do lado do repositório público (para quem prefere
declarar a dependência via manifest, ex. em CI):
// Package.swift do app hospedeiro
dependencies: [
.package(url: "https://github.com/metrikehub/metrike-ads-ios", from: "0.1.1")
]
// target do app hospedeiro
.target(
name: "MeuApp",
dependencies: [
.product(name: "MetrikeAds", package: "metrike-ads-ios")
]
)
E o que esse pacote público resolve por baixo (não precisa copiar isto — é
só para entender o mecanismo): um binaryTarget apontando para o zip do
cdx, com checksum publicado a cada release:
// Package.swift do repositório público (referência, não edite)
.binaryTarget(
name: "MetrikeAds",
url: "https://cdx.metrike.com.br/sdk/ios/MetrikeAds-0.1.1.zip",
checksum: "<checksum publicado no release>"
)
- Piso: iOS 14 (~98% dos aparelhos ativos — mesmo piso do Google IMA iOS).
- Um único produto,
MetrikeAds— ao contrário do Android (que separacore/videopelo peso do Media3), aqui o player de vídeo é AVFoundation, parte do sistema, então vídeo não adiciona dependência nenhuma. Um zip, um checksum, um framework. - Zero dependências externas — HTTP via
URLSession, JSON viaJSONSerialization, XML do VAST viaXMLParser, tudo do sistema. Nem a exceção que o Android tinha (com.android.installreferrer) existe aqui — não há Install Referrer no iOS (§6.1). - Assinatura de binário não é exigida para
binaryTargetdo SPM — o checksum é a garantia de integridade. - Apps Objective-C: toda a API pública é
@objc/herda deNSObject; delegates sãoprotocols (padrão da plataforma), nunca closures na superfície pública. Sem generics, enums-com-payload ou structs Swift-only na API — a mesma disciplina de interop que o SDK Android tem com Java.
2. Inicialização e identidade automática
// Swift — identidade automática (recomendado)
Metrike.initialize()
// Swift — com override do nome do app
let config = MetrikeConfig()
config.appName = "MeuApp"
Metrike.initialize(config: config)
// Objective-C — ATENÇÃO: use mk_initialize, não initialize. Todo NSObject já
// responde a +initialize (hook do runtime Objective-C, chamado
// automaticamente na primeira vez que a classe recebe qualquer mensagem) --
// [Metrike initialize] COMPILA (é herdado de NSObject) mas não roda uma
// linha sequer do SDK. Por isso Metrike.swift renomeia esse método
// especificamente para o seletor mk_initialize (@objc(mk_initialize)) do
// lado Swift — é a chamada errada mais fácil de copiar sem perceber.
[Metrike mk_initialize];
// com override — MetrikeConfig não tem inicializador nomeado (é um
// NSObject com propriedades @objc mutáveis, não um struct/data class com
// argumentos default) — configure propriedade por propriedade. Este
// overload recebe um argumento, então o seletor já é distinto de
// +initialize e não precisa de nenhum rename.
MetrikeConfig *config = [MetrikeConfig new];
config.appName = @"MeuApp";
[Metrike initializeConfig:config];
Chame uma vez, cedo — application(_:didFinishLaunchingWithOptions:) no
AppDelegate é o lugar recomendado (é onde o app de exemplo faz).
Do lado Objective-C, o seletor é mk_initialize, não initialize — ver
o comentário no snippet acima. [Metrike initialize] compila silenciosamente
sem inicializar nada (colisão com o hook +initialize do próprio
NSObject); toda chamada subsequente do SDK vira no-op porque
isInitialized() nunca fica true.
O que é coletado automaticamente (via Bundle.main, sem nenhuma
declaração manual):
| Campo | Origem |
|---|---|
| app_bundle | Bundle.main.bundleIdentifier — autoritativo |
| app_name | CFBundleDisplayName/CFBundleName (ou o appName do MetrikeConfig, se informado) |
| app_version | CFBundleShortVersionString |
| sdk_version | versão do SDK (metrike-ios/x.y.z) |
| device_type | phone / tablet, via UIDevice.current.userInterfaceIdiom |
| visitor_id | UUID gerado pelo SDK, persistido em UserDefaults — equivalente do cookie _mk_vid da tag web. Não existe com Metrike.setTrackingEnabled(false) (§5) |
| session_id | UUID novo por processo |
Esses quatro campos de identidade (app_bundle/app_name/app_version/
sdk_version) acompanham toda requisição — serve, beacons, tracker VAST,
clique e conversão passam pelo mesmo seam de identidade (UrlBuilder), sem
exceção.
O que NÃO é coletado:
- IDFA — o SDK não toca o identificador de publicidade da Apple. Sem IDFA, sem o prompt de App Tracking Transparency (§8).
- Localização — só se o publisher optar explicitamente (§5).
Metrike.isInitialized() reporta se initialize() já rodou. Toda API
pública é uma borda defensiva (guard/optionals) — Swift não tem um
catch-all de exceção como a JVM, então a postura aqui é código defensivo
sistemático nas bordas em vez de um wrapper mágico: uma falha interna nunca
derruba o app hospedeiro, na pior hipótese uma chamada vira no-op.
visitor_id é persistido em UserDefaults (não Keychain) — reinstalou,
novo visitante, igual Android/web. Sobreviver a reinstall seria mais
rastreio, não menos; foi uma escolha deliberada.
3. Formatos de anúncio
| Formato | Classe | Endpoint | Base |
|---|---|---|---|
| Banner inline | MetrikeBannerView | /serve | UIView + WKWebView interno |
| Interstitial fullscreen | MetrikeInterstitial | /serve | UIViewController fullscreen próprio |
| Vídeo fullscreen (pre/mid/post-roll manual) | MetrikeVastVideo | /vast/{token} | AVPlayer em view controller do SDK |
| Vídeo outstream inline | MetrikeVideoAdView | /vast/{token} | AVPlayer inline |
| Instream automático (estilo IMA) | MetrikeAdsLoader | /vmap/{token} ou /vast/{token} | scheduler próprio sobre o AVPlayer do publisher |
| Player próprio do publisher | Metrike.buildVastUrl(token) | /vast/{token} | — |
Contrato do delegate —
weak var delegate, sempre retenha. Igual aoWeakReferenceque o Android usa pro mesmo fim, cadadelegatedeste SDK éweakdo lado do SDK (evita um ciclo forte SDK↔host). Se nada além dessa propriedadeweaksegurar o objeto delegate, o ARC libera ele assim que a função que o criou retorna — e nenhum callback chega a disparar, porque a chamada de rede que disparariaonReady/onLoad/etc. ainda está em voo. Os exemplos abaixo usam dois padrões, ambos corretos:
- Banner e interstitial (um formato só por tela, protocolo próprio): o
UIViewControllerconforma ao protocolo direto e passaself— o VC já é mantido vivo pela apresentação da tela, nada extra a reter.- Os três formatos de vídeo (
MetrikeVastVideo/MetrikeVideoAdView/MetrikeAdsLoader) compartilham o mesmo protocoloMetrikeVideoDelegate— uma tela real costuma ter mais de uma instância ao mesmo tempo (vários outstreams num feed, por exemplo), e uma única conformance no VC não distinguiria qual anúncio disparou qual callback. Para esses três, use um objeto delegate dedicado por instância, guardado como propriedade (nunca umalet/varlocal dentro da função) — o mesmo padrão que o app de exemplo (Example/) usa de verdade.
3.1 Banner (MetrikeBannerView)
// Swift — o VC conforma ao delegate direto e passa self (formato único por
// tela — ver nota acima).
final class ArticleViewController: UIViewController, MetrikeBannerViewDelegate {
private var banner: MetrikeBannerView? // referência forte -- também facilita destroy() depois
override func viewDidLoad() {
super.viewDidLoad()
let banner = MetrikeBannerView(frame: CGRect(x: 0, y: 0, width: 320, height: 50))
banner.zoneId = 12345
banner.contentUrl = "https://meusite.com.br/noticia/123" // opcional — ver §4
banner.delegate = self
view.addSubview(banner)
self.banner = banner
banner.load()
}
func onLoad() { /* renderizado */ }
func onNoFill() { /* a view já colapsa sozinha */ }
func onClick() { /* SFSafariViewController já foi aberto */ }
func onClosed() { }
func onError(_ code: Int, _ message: String) { /* log/telemetria */ }
func onViewable(_ timeToViewableMs: Int64) { /* beacon nativo já disparado */ }
}
// Objective-C — mesma ideia: o próprio ViewController conforma ao protocolo.
@interface ArticleViewController () <MetrikeBannerViewDelegate>
@property (nonatomic, strong) MetrikeBannerView *banner;
@end
@implementation ArticleViewController
- (void)viewDidLoad {
[super viewDidLoad];
MetrikeBannerView *banner = [[MetrikeBannerView alloc] initWithFrame:CGRectMake(0, 0, 320, 50)];
banner.zoneId = 12345;
banner.contentUrl = @"https://meusite.com.br/noticia/123";
banner.delegate = self;
[self.view addSubview:banner];
self.banner = banner;
[banner load];
}
- (void)onLoad { }
- (void)onNoFill { }
- (void)onClick { }
- (void)onClosed { }
- (void)onError:(NSInteger)code message:(NSString *)message { }
- (void)onViewable:(int64_t)timeToViewableMs { }
@end
Só onError é obrigatório no delegate — os demais métodos são @objc optional, então implemente só o que interessa (diferente do Android, onde
os listeners Kotlin exigem todos os métodos em Java por causa de como o
compilador Kotlin gera bytecode — ver o aviso equivalente no doc Android;
essa armadilha específica não existe aqui).
O creative é desenhado num WKWebView interno (image vira <img>,
htmlzip/ext_tag carregam o HTML), mas medição e beacons são 100%
nativos — viewability, impressão e clique não dependem de nenhum
IntersectionObserver rodando dentro do WebView. Remover a view da
hierarquia (removeFromSuperview) derruba o creative atual por completo;
para reaparecer, chame load() de novo. Cobrir a tela temporariamente com
outra tela (um MetrikeInterstitial/MetrikeVastVideo do próprio SDK, ou
navegação comum do host) não derruba nada — a view continua intacta e
volta a aparecer sozinha.
MRAID: o mesmo metrike-mraid.js do Android é reaproveitado (é JS puro)
— só o lado nativo da bridge (WKScriptMessageHandler/WKUserScript) é
próprio do iOS. Mesmo perfil core, mesmas exclusões (mraid.getLocation
fora — §10). Nenhuma API pública adicional: se a zona tiver um creative
MRAID configurado, o SDK já injeta e faz o host da bridge sozinho dentro do
WKWebView interno.
ext_tag: o servidor já pré-substitui qualquer macro de clique no HTML
antes de enviá-lo — o SDK apenas injeta o HTML no WKWebView tal como veio.
3.2 Interstitial (MetrikeInterstitial)
// Swift — mesma ideia do banner: o VC conforma direto (protocolo próprio,
// não compartilhado com outro formato).
final class ArticleViewController: UIViewController, MetrikeInterstitialDelegate {
private var interstitial: MetrikeInterstitial? // referência forte -- preload() é assíncrono
func loadInterstitial() {
let ad = MetrikeInterstitial(zoneId: 12345)
ad.closeDelaySeconds = 5 // default
ad.delegate = self
interstitial = ad
ad.preload()
}
func onReady() {
interstitial?.show(from: self) // no-op se já não estiver mais pronto
}
func onClosed() { /* usuário fechou ou voltou */ }
func onNoFill() { }
func onError(_ code: Int, _ message: String) { }
func onClick() { }
}
// Objective-C
@interface ArticleViewController () <MetrikeInterstitialDelegate>
@property (nonatomic, strong) MetrikeInterstitial *interstitial;
@end
@implementation ArticleViewController
- (void)loadInterstitial {
MetrikeInterstitial *ad = [[MetrikeInterstitial alloc] initWithZoneId:12345];
ad.closeDelaySeconds = 5;
ad.delegate = self;
self.interstitial = ad; // referência forte -- preload() é assíncrono
[ad preload];
}
- (void)onReady { [self.interstitial showFrom:self]; }
- (void)onClosed { }
- (void)onNoFill { }
- (void)onError:(NSInteger)code message:(NSString *)message { }
- (void)onClick { }
@end
preload()/show(from:) seguem o mesmo modelo pre-fetch do AdButler:
chamar show(from:) antes de onReady (ou depois de já ter sido mostrado)
é um no-op silencioso reportado via onError — não há um Bool de retorno
como o isReady() do Android, onError é o único canal de feedback para
essa borda. Chame preload() de novo antes de tentar mostrar outra vez. O
botão de fechar (X) fica oculto até o delay configurado se esgotar
(closeDelaySeconds, default 5) — nunca apenas desabilitado, sempre
removido do hit-testing até então.
Nota: ao contrário do banner, MetrikeInterstitial não aceita
contentUrl nesta versão — a requisição /serve do interstitial não leva
esse parâmetro. Use um banner (ou o classificador app-level do §4) se
precisar de classificação por conteúdo em telas cujo único formato é
interstitial.
3.3 Vídeo fullscreen manual — pre/mid/post-roll (MetrikeVastVideo)
Modelo AdButler: preload() → onReady() → display(from:). O SDK não
decide quando mostrar o anúncio — quem decide o timing (pré-roll, meio do
conteúdo, pós-roll) é o publisher, chamando display(from:) no momento
certo.
// Swift — mid-roll pausando o próprio player de conteúdo do publisher.
// MetrikeVideoDelegate é compartilhado pelos 3 formatos de vídeo (§3.3-3.5)
// -- um objeto delegate dedicado por instância, guardado como PROPRIEDADE
// (nunca local), é o padrão usado aqui e no app de exemplo (ver nota no
// início de §3).
final class VastDelegate: NSObject, MetrikeVideoDelegate {
weak var presenter: UIViewController?
let vast: MetrikeVastVideo
let contentPlayer: AVPlayer
init(vast: MetrikeVastVideo, contentPlayer: AVPlayer, presenter: UIViewController) {
self.vast = vast
self.contentPlayer = contentPlayer
self.presenter = presenter
}
func onReady() {
contentPlayer.pause() // pausa o conteúdo ANTES de mostrar o ad
guard let presenter else { return }
vast.display(from: presenter)
}
func onComplete() { contentPlayer.play() } // retoma
func onClose() { contentPlayer.play() } // fechou antes do fim — retoma também
func onError(_ code: Int, _ message: String) { contentPlayer.play() }
func onStart() { }
func onQuartile(_ name: String) { } // "firstQuartile" / "midpoint" / "thirdQuartile"
func onSkip() { }
func onClick() { }
}
final class ArticleViewController: UIViewController {
private var vastVideo: MetrikeVastVideo?
private var vastDelegate: VastDelegate? // propriedade -- vast.delegate é weak
func loadMidRollAd() {
let vast = MetrikeVastVideo(vastToken: "abc123")
let delegate = VastDelegate(vast: vast, contentPlayer: myContentPlayer, presenter: self)
vast.delegate = delegate
vastVideo = vast
vastDelegate = delegate
// Chame preload() com antecedência (ex.: ao chegar perto do ponto de meio de conteúdo)
vast.preload()
}
}
// Objective-C
@interface VastDelegate : NSObject <MetrikeVideoDelegate>
@property (nonatomic, weak) UIViewController *presenter;
@property (nonatomic, strong) MetrikeVastVideo *vast;
@property (nonatomic, strong) AVPlayer *contentPlayer;
@end
@implementation VastDelegate
- (void)onReady {
[self.contentPlayer pause];
[self.vast displayFrom:self.presenter];
}
- (void)onComplete { [self.contentPlayer play]; }
- (void)onClose { [self.contentPlayer play]; }
- (void)onError:(NSInteger)code message:(NSString *)message { [self.contentPlayer play]; }
- (void)onStart { }
- (void)onQuartile:(NSString *)name { }
- (void)onSkip { }
- (void)onClick { }
@end
@interface ArticleViewController ()
@property (nonatomic, strong) MetrikeVastVideo *vastVideo;
@property (nonatomic, strong) VastDelegate *vastDelegate; // propriedade -- vast.delegate é weak
@end
@implementation ArticleViewController
- (void)loadMidRollAd {
MetrikeVastVideo *vast = [[MetrikeVastVideo alloc] initWithVastToken:@"abc123"];
VastDelegate *delegate = [VastDelegate new];
delegate.vast = vast;
delegate.contentPlayer = self.myContentPlayer;
delegate.presenter = self;
vast.delegate = delegate;
self.vastVideo = vast;
self.vastDelegate = delegate;
[vast preload];
}
@end
Pre-roll: chame preload()/display(from:) antes de iniciar o conteúdo.
Post-roll: chame no evento de fim de conteúdo, e não retome nada em
onComplete()/onClose().
Comportamentos do player fullscreen que você não precisa (nem consegue)
configurar: um countdown "Anúncio · m:ss" e uma barra de progresso ficam
sempre visíveis; um clique no "Saiba mais" abre o destino num
SFSafariViewController pausando o anúncio, que retoma sozinho quando o
usuário volta (equivalente exato do Custom Tabs do Android — sem risco de
ad "congelado").
3.4 Vídeo outstream inline (MetrikeVideoAdView)
Sem view controller próprio — uma UIView que você coloca direto no seu
layout (ex.: uma linha de feed). Autoplay mudo quando ≥50% visível
continuamente, pausa ao sair de 50%; som ativa no toque do próprio usuário
no ícone. Ao terminar, a view colapsa sozinha — não há botão de fechar nem
skip (a saída padrão de mercado do outstream é o próprio scroll levando o
anúncio pra fora de tela).
// Swift — mesmo padrão de §3.3: delegate dedicado, guardado como
// propriedade. Um feed tipicamente tem VÁRIOS outstreams simultâneos, então
// o VC não pode conformar direto a MetrikeVideoDelegate (uma única
// conformance não distingue qual instância disparou qual callback).
final class OutstreamDelegate: NSObject, MetrikeVideoDelegate {
func onReady() { }
func onError(_ code: Int, _ message: String) { }
// demais callbacks (onStart, onQuartile, onComplete, onClick...) — mesmo
// protocol MetrikeVideoDelegate do fullscreen; todos @objc optional
// menos onError
}
final class FeedCell: UICollectionViewCell {
private var outstreamAd: MetrikeVideoAdView?
private var outstreamDelegate: OutstreamDelegate? // propriedade -- delegate é weak
func loadOutstream(vastToken: String) {
let outstream = MetrikeVideoAdView(frame: contentView.bounds)
outstream.vastToken = vastToken
let delegate = OutstreamDelegate()
outstream.delegate = delegate
contentView.addSubview(outstream)
outstreamAd = outstream
outstreamDelegate = delegate
outstream.load()
}
}
// Objective-C
@interface OutstreamDelegate : NSObject <MetrikeVideoDelegate>
@end
@implementation OutstreamDelegate
- (void)onReady { }
- (void)onError:(NSInteger)code message:(NSString *)message { }
@end
@interface FeedCell : UICollectionViewCell
@property (nonatomic, strong) MetrikeVideoAdView *outstreamAd;
@property (nonatomic, strong) OutstreamDelegate *outstreamDelegate; // propriedade -- delegate é weak
@end
@implementation FeedCell
- (void)loadOutstream:(NSString *)vastToken {
MetrikeVideoAdView *outstream = [[MetrikeVideoAdView alloc] initWithFrame:self.contentView.bounds];
outstream.vastToken = vastToken;
OutstreamDelegate *delegate = [OutstreamDelegate new];
outstream.delegate = delegate;
[self.contentView addSubview:outstream];
self.outstreamAd = outstream;
self.outstreamDelegate = delegate;
[outstream load];
}
@end
Chame destroy() (ou deixe a view sair da hierarquia via
removeFromSuperview) para parar o player e a medição de viewability — a
reciclagem de uma UICollectionView/UITableView já dispara isso sozinha.
Cobrir a tela temporariamente com outra tela (interstitial, VAST fullscreen
do próprio SDK, ou navegação do host) não derruba a view.
3.5 Instream automático estilo IMA (MetrikeAdsLoader)
Requisito obrigatório: o player de conteúdo do publisher precisa ser
AVPlayer — é o player de facto do iOS, então na prática isso cobre quase
todo mundo (análogo ao "Media3-only" do Android). Não há no AVFoundation um
plug-point equivalente ao AdsLoader do Media3 que insira anúncios
diretamente na timeline do player — este formato é um scheduler próprio,
mesmo modelo IMA-like: pausa o conteúdo no break, mostra o ad num AVPlayer
separado sobre um container que você fornece, e retoma o conteúdo ao fim —
inclusive se o ad falhar (resiliência é o objetivo central desta
classe).
// Swift — mesmo padrão de §3.3/3.4: delegate dedicado, guardado como
// propriedade (loader.delegate é weak).
final class InstreamDelegate: NSObject, MetrikeVideoDelegate {
func onStart() { /* um break começou -- torne o overlay interativo se precisar */ }
func onComplete() { }
func onSkip() { }
func onClick() { }
func onError(_ code: Int, _ message: String) { /* break abandonado -- conteúdo já retomou sozinho */ }
}
final class InstreamViewController: UIViewController {
private let contentPlayer = AVPlayer(url: contentVideoUrl)
private var adsLoader: MetrikeAdsLoader?
private var instreamDelegate: InstreamDelegate?
override func viewDidLoad() {
super.viewDidLoad()
let playerLayer = AVPlayerLayer(player: contentPlayer)
playerLayer.frame = contentContainerView.bounds
contentContainerView.layer.addSublayer(playerLayer)
let loader = MetrikeAdsLoader(vmapToken: "abc123")
let delegate = InstreamDelegate()
loader.delegate = delegate
// overlayContainerView é onde CADA break embute seu próprio AVPlayer +
// chrome (countdown, progresso, skip/clique) só durante aquele break.
loader.attach(to: contentPlayer, overlayContainer: overlayContainerView, from: self)
adsLoader = loader
instreamDelegate = delegate
contentPlayer.play()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
adsLoader?.detach()
contentPlayer.pause()
}
}
// Objective-C
@interface InstreamDelegate : NSObject <MetrikeVideoDelegate>
@end
@implementation InstreamDelegate
- (void)onStart { }
- (void)onComplete { }
- (void)onSkip { }
- (void)onClick { }
- (void)onError:(NSInteger)code message:(NSString *)message { }
@end
@interface InstreamViewController ()
@property (nonatomic, strong) AVPlayer *contentPlayer;
@property (nonatomic, strong) MetrikeAdsLoader *adsLoader;
@property (nonatomic, strong) InstreamDelegate *instreamDelegate; // propriedade -- loader.delegate é weak
@end
@implementation InstreamViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.contentPlayer = [AVPlayer playerWithURL:contentVideoUrl];
AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.contentPlayer];
playerLayer.frame = self.contentContainerView.bounds;
[self.contentContainerView.layer addSublayer:playerLayer];
MetrikeAdsLoader *loader = [[MetrikeAdsLoader alloc] initWithVmapToken:@"abc123"];
InstreamDelegate *delegate = [InstreamDelegate new];
loader.delegate = delegate;
[loader attachTo:self.contentPlayer overlayContainer:self.overlayContainerView from:self];
self.adsLoader = loader;
self.instreamDelegate = delegate;
[self.contentPlayer play];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self.adsLoader detach];
[self.contentPlayer pause];
}
@end
vmapToken aponta pro /vmap/{token} — o servidor pode responder um
<vmap:VMAP> com múltiplos breaks OU, para uma zona sem breaks configurados,
um <VAST> único (nesse caso o loader trata como um pre-roll síncrono, sem
fazer um segundo fetch). Pós-roll dispara no fim de conteúdo (fim de item do
AVPlayer), não por posição. Um break individual que falhar (rede,
VAST malformado, sem preenchimento) nunca deixa o conteúdo travado pausado —
o conteúdo é resumido automaticamente; se nada em tela avançar por 15s
seguidos durante um break (rede caiu no meio, etc.), um watchdog interno
força o fim do break e resume o conteúdo de qualquer forma.
Player diferente de AVPlayer (ex.: um player de terceiro embutido via
outra lib) não é compatível com este formato — use o modelo fullscreen
manual (§3.3) ou buildVastUrl() (§3.6) nesse caso.
3.6 Player próprio (Metrike.buildVastUrl)
Para publishers com player de vídeo próprio que não usam nenhum dos formatos acima:
// Swift
if let vastUrl = Metrike.buildVastUrl("abc123") {
// vastUrl já vem com app_bundle/app_name/app_version/sdk_version anexados —
// alimente-o no seu player como faria com qualquer VAST tag.
}
// Objective-C
NSString *vastUrl = [Metrike buildVastUrl:@"abc123"];
if (vastUrl != nil) {
// ...
}
Trade-off explícito: o SDK não mede nada nesse caminho (sem beacons
nativos de viewability/audibilidade) — o ad request deixa de ser anônimo
(identidade do app anexada), mas a medição vira responsabilidade do player
externo, exatamente como um VAST tag puro contratado de qualquer fornecedor.
buildVastUrl retorna nil se chamado antes de Metrike.initialize() ou
com um token vazio.
4. contentUrl — classificação por conteúdo
MetrikeBannerView.contentUrl é a URL web equivalente do conteúdo da tela
atual (apps de notícias tipicamente têm uma). O servidor trata como
page_url — o mesmo classificador de conteúdo que já roda na tag web
funciona sem nenhuma mudança, por matéria, igual à experiência web.
Sem contentUrl (ou em telas que só usam interstitial/vídeo, que não
carregam esse parâmetro nesta versão), o servidor cai num caminho app-level:
resolve a categoria pela iTunes Lookup API pública
(itunes.apple.com/lookup?bundleId=..., que devolve primaryGenreName)
para o app_bundle do seu app — equivalente iOS do scrape da página pública
da Play Store que o Android usa, cacheado por app. Preencha contentUrl
sempre que a tela tiver um equivalente web identificável — é estritamente
melhor que o fallback app-level.
5. Consent, privacidade e GPS
Consent (IABTCF): o SDK lê IABTCF_gdprApplies e IABTCF_TCString do
UserDefaults.standard — é onde todo CMP in-app certificado IAB escreve — e
repassa como gdpr/gdpr_consent no /serve, automaticamente, sem nenhuma
chamada extra do publisher. Sem CMP presente, o SDK se comporta como a tag
web sem TCF (nenhum dos dois parâmetros é enviado).
IDFA: não é tocado. Sem leitura do identificador de publicidade, sem
prompt de App Tracking Transparency por causa deste SDK (§8). visitor_id
é um UUID próprio persistido em UserDefaults, não um device ID.
Recusa do usuário (opt-out) e o identificador do SDK: o visitor_id é
um UUID gerado pelo SDK e guardado só no UserDefaults do seu app — dois
apps no mesmo aparelho têm IDs diferentes. Para ligar a escolha de
privacidade do seu app ao SDK:
// Swift — pode ser chamado antes ou depois do initialize()
Metrike.setTrackingEnabled(false) // usuário recusou
Metrike.isTrackingEnabled() // estado atual
Metrike.visitorId() // ID atual, ou nil
// Objective-C
[Metrike setTrackingEnabled:NO];
NSString *visitorId = [Metrike visitorId]; // pode ser nil
Com setTrackingEnabled(false):
- o UUID guardado é apagado e nenhum novo é gerado;
- nenhuma URL leva
visitor_id(elas passam a levaruntracked=1) e nenhum beacon levavisitor_id/session_id; - nenhuma localização é enviada, mesmo com
collectLocation = true; - os anúncios continuam carregando e impressões e cliques continuam contados, só sem identificador.
A escolha fica gravada e vale nas próximas aberturas do app. Chamada antes
do initialize(), ela é aplicada antes de qualquer ID existir: um app que
já começa com a recusa nunca gera o UUID. setTrackingEnabled(true) volta a
permitir, com um ID novo, sem ligação com o anterior.
O mesmo vale por zona: quando o /serve responde tracking: false (zona
que exige consentimento e ele não veio), o SDK não manda o ID nos beacons e
no clique daquele anúncio, nem no próximo pedido daquela zona.
As requisições do SDK ao servidor da Metrike não usam o cookie jar
compartilhado do URLSession: o ID só viaja quando o SDK o envia
explicitamente.
Metrike.visitorId() devolve o ID que o SDK envia, para o app mostrar ao
usuário numa tela de privacidade ("meus dados"). É esse ID que o titular
informa ao pedir acesso ou exclusão dos dados ligados a ele. Devolve nil
antes do initialize() e com o tracking desligado.
App dirigido a crianças ou adolescentes: se o seu app é para esse público (por exemplo, na Kids Category da App Store), declare na inicialização:
// Swift
let config = MetrikeConfig()
config.childDirected = true
Metrike.initialize(config: config)
// Objective-C
MetrikeConfig *config = [MetrikeConfig new];
config.childDirected = YES;
[Metrike initializeConfig:config];
Com childDirected = true, o SDK não gera nem envia o UUID de instalação
(mesmo que setTrackingEnabled(true) seja chamado), não lê localização
(mesmo com collectLocation = true) e manda child_directed=1 em toda
requisição ao servidor da Metrike, que aplica a mesma regra do lado dele. Os
anúncios continuam sendo exibidos e contados. Avise também a Metrike para
marcar o veículo ou a zona como infantil no painel.
GPS — opt-in, default OFF:
// Swift
let config = MetrikeConfig()
config.collectLocation = true
Metrike.initialize(config: config)
// Objective-C
MetrikeConfig *config = [MetrikeConfig new];
config.collectLocation = YES;
[Metrike initializeConfig:config];
Com collectLocation = true, o SDK lê a última localização conhecida em
cache de um CLLocationManager compartilhado — nunca chama
requestWhenInUseAuthorization(), nunca chama
startUpdatingLocation() — e somente se o app hospedeiro já detém
autorização authorizedWhenInUse/authorizedAlways. Sem uma das duas,
collectLocation = true é um no-op silencioso.
Finalidade e precisão da localização: a localização serve só para
verificar tráfego inválido — comparar a posição do aparelho com a
geolocalização pelo IP ajuda a achar emulador, proxy e fraude de origem. Os
relatórios de praça continuam vindo do IP. Antes de sair do aparelho, a
coordenada é arredondada para 2 casas decimais (cerca de 1,1 km, nível de
bairro) e a precisão informada nunca é menor que 1.000 m. A localização
só vai em beacon rastreado: com setTrackingEnabled(false), numa zona que
respondeu tracking: false ou em app infantil (childDirected), ela não é
enviada.
Aviso de App Privacy (App Store Connect): se o publisher ativar
collectLocation, a seção "App Privacy" do app na App Store Connect precisa
refletir a coleta de localização aproximada (Coarse Location), com
finalidade App Functionality (prevenção de fraude). O manifesto do SDK já
declara esse tipo (§8); o formulário da App Store Connect continua sendo
responsabilidade do publisher.
6. Conversão in-app
Lado do publisher (onde o anúncio é exibido)
Nada a fazer. O clique já abre a URL de /click via
SFSafariViewController; o redirect 302 do servidor já anexa mk_cid na
própria URL de destino. Não há API adicional do lado de quem exibe o
anúncio.
Lado do advertiser (app anunciado no clique)
O app do advertiser também integra MetrikeAds e usa dois pontos de
entrada:
1. Capturar o mk_cid — deep link (universal link ou custom scheme):
// Swift — AppDelegate
func application(_ app: UIApplication, open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
return Metrike.handleDeepLink(url) // lê mk_cid da URL, se houver
}
// Universal links chegam via NSUserActivity, não open url: —
// exige Associated Domains (applinks:) + apple-app-site-association
// servido no domínio de destino.
func application(_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL else { return false }
return Metrike.handleDeepLink(url)
}
// Objective-C
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
return [Metrike handleDeepLink:url];
}
- (BOOL)application:(UIApplication *)application
continueUserActivity:(NSUserActivity *)userActivity
restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> *))restorationHandler {
if (![userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]
|| userActivity.webpageURL == nil) {
return NO;
}
return [Metrike handleDeepLink:userActivity.webpageURL];
}
Chame no AppDelegate do app do advertiser. Retorna true se um mk_cid
foi encontrado e persistido; um link sem esse parâmetro não apaga um
mk_cid já armazenado por outro caminho.
2. Registrar o evento de conversão:
// Swift
Metrike.trackConversion("goal_public_id", valueCents: 4990, orderId: "order-123")
// Objective-C
[Metrike trackConversion:@"goal_public_id" valueCents:@4990 orderId:@"order-123"];
valueCentsé umNSNumber?/Int64em centavos (espelhaconversion_goals.value_centsno servidor) — mas isso é só a convenção da assinatura Swift/Obj-C do método; no wire ele é convertido para unidades decimais de moeda antes de sair (4990→value=49.90, nãovalue=4990), porque o parser do servidor espera decimal e multiplica por 100 internamente, igual à tag web e ao SDK Android.- Um
valueCentsnegativo é omitido por completo da requisição (mesmo efeito de não passar nada), porque o servidor rejeita valores negativos de qualquer forma. orderIdé opcional, string livre (para dedup do lado do servidor, se o goal estiver configurado para isso).- Atribuição, deduplicação e janela de atribuição são inteiramente do
servidor — nada muda no comportamento existente do
/pixel/conversion.
Atribuição de instalação no iOS
Honestidade primeiro: não existe atribuição individual de clique→instalação no iOS, por decisão de plataforma da Apple, não por limitação deste SDK.
O Android tem o Play Install Referrer — um mecanismo do sistema que carrega
o mk_cid do clique através da instalação e da primeira abertura do app. A
App Store não tem equivalente: nenhum "carteiro" do outro lado. Ao
contrário do Google, a Apple deliberadamente não expõe um jeito de ligar
"este usuário clicou neste anúncio" a "este usuário instalou este app" —
essa é uma política de privacidade da plataforma, não uma lacuna técnica.
Como consequência, este SDK não implementa nenhum substituto: sem
pasteboard, sem fingerprinting probabilístico, sem SKAdNetwork no V1 —
esses caminhos existem no mercado justamente para contornar essa ausência, e
foram descartados de propósito (política de privacidade da Apple em torno de
fingerprinting, e SKAdNetwork/AdAttributionKit são agregados por design,
não individuais — não valeria a complexidade para o V1).
O que funciona integralmente: conversão de usuários que já têm o app
instalado — deep link (universal link ou custom scheme) +
Metrike.trackConversion(...) cobre esse caminho por completo, exatamente
como no Android. A lacuna é especificamente "clique levou a uma instalação
nova" — não "clique levou a uma conversão".
Instalação não é evento contável — não existe (e não está planejado) um
goal type install, mesmo caminho do Android.
Caminho futuro (V1.1, se algum dia importar): SKAdNetwork/
AdAttributionKit dão atribuição agregada (contagens por campanha, sem
identificar o usuário individual) — diferente do que o Install Referrer
Android oferece, mas é o único caminho que a Apple permite. Não está no
escopo do V1.
7. Métricas
Paridade total com a tag web e com o SDK Android — mesmo pipeline de ingestão, mesmos dashboards, mesmo verification report:
| Evento | Disparado por |
|---|---|
| request | load() (banner) / preload() (interstitial) — cada fill bem-sucedido de GET /vast/{token} já grava sua própria linha request no servidor, então zonas de vídeo contam requests + uniques igual às demais |
| impression | banner/interstitial renderizados; vídeo ao iniciar (pixel <Impression> do VAST) |
| viewable / notViewable / viewUndetermined | medição nativa de viewability (abaixo) |
| click | toque no creative (abre /click via SFSafariViewController) |
| Quartis VAST (start, firstQuartile, midpoint, thirdQuartile, complete) | polling de posição do player, todos os formatos de vídeo |
| mute / unmute / skip | interação do usuário no player de vídeo |
| Conversão | Metrike.trackConversion (lado advertiser) |
Exclusivas do SDK (não existem no caminho web-tag/in-app-sem-SDK):
identidade do app (app_bundle/app_name/app_version), sdk_version
(prova de ambiente in_app_mechanism='sdk' — topo da escada de detecção
in-app, acima de mraid-top/mraid-window/webview-ua), viewability com
oclusão/tela/foreground reais (não estimada por UA), audibilidade real,
device_type declarado (não inferido por UA).
Viewability (display — banner/interstitial): regra MRC, ≥50% da área
visível por 1s contínuo, medida nativamente por amostragem CADisplayLink
(throttled ~250ms): frame convertido pra janela × bounds da tela,
isHidden/alpha/hierarquia, janela ativa. viewable_mechanism = 'native-sdk'. Timeout de 30min sem atingir o critério: comportamento
silencioso — igual à tag web e ao Android, nenhum beacon é disparado nesse
desfecho.
Viewability + audibilidade (vídeo): ≥50% visível por 2s contínuos
(MRC vídeo). Audibilidade combina AVPlayer.volume/isMuted × volume do
device (AVAudioSession.outputVolume). Quando visível≥50% e audível
somam, juntos, pelo menos metade da duração do vídeo, dispara
fully_viewable_audible_half_duration_impression.
Duração de referência (quartis + audibilidade): o SDK usa a duração
real reportada pelo player assim que conhecida — a <Duration> do XML é
só fallback inicial. Igual ao Google IMA e ao Android, imune a metadado
errado no ad item.
Mecanismos: todo beacon do SDK carrega in_app_mechanism='sdk'
(display/interstitial) — prova positiva de identidade, diferente de
mraid-top/mraid-window/webview-ua (heurísticas de UA sem identidade).
Identidade no pipeline de vídeo: os pixels de tracking/impressão/click
que voltam dentro do próprio XML VAST (<Impression>, <Tracking event="...">, <ClickTracking>) são GETs simples, sem corpo JSON — o SDK
anexa a identidade a cada um deles imediatamente antes de disparar, mas só
quando a URL aponta pro nosso próprio servidor (mesmo host do baseUrl
configurado). Um break VMAP resolvido pra um ad server de terceiro nunca
recebe essa identidade — só os pixels que o /vast//serve da própria
Metrike gerou.
8. Privacidade e o PrivacyInfo.xcprivacy
O framework embute um manifesto de privacidade
(PrivacyInfo.xcprivacy) declarando o que o SDK coleta — a revisão da App
Store rejeita apps cujos SDKs não o declaram, então isso é parte do
artefato, não da documentação. Um app hospedeiro que integra MetrikeAds
herda essa declaração automaticamente; nada a configurar do lado do
publisher. O manifesto declara:
- Sem tracking (
NSPrivacyTracking = false, sem domínios de tracking declarados) — este SDK não faz cross-app/cross-site tracking. Ovisitor_idé um UUID por instalação, guardado noUserDefaultsdo próprio app: o mesmo aparelho tem um ID diferente em cada app, e o SDK não lê IDFA nem IDFV. Por isso o alcance de uma campanha que roda em apps de empresas diferentes não junta a mesma pessoa entre eles. - Identificador próprio do SDK (
visitor_id) — coletado, não linkado à identidade do usuário, não é tracking, propósito Analytics. - Dados de uso publicitário (impressões/cliques) — coletados, não
linkados, não é tracking, propósito Publicidade de terceiros
(
ThirdPartyAdvertising). - Localização aproximada (
CoarseLocation) — só quando o app ligacollectLocatione já tem a permissão; arredondada no aparelho a 2 casas (~1,1 km) antes de sair, por isso aproximada e não precisa. Não linkada, não é tracking, propósitoAppFunctionality(prevenção de fraude). - API de razão exigida:
UserDefaults(categoriaCA92.1) — ondevisitor_ide o consent IABTCF são persistidos.
Sem IDFA → sem prompt de ATT. Como o SDK nunca toca o identificador de publicidade, o prompt de App Tracking Transparency nunca aparece por causa deste SDK — uma vantagem competitiva documentável para o publisher.
9. Requisitos
- iOS 14+.
- Swift e Objective-C — API pública 100% consumível dos dois lados
(
@objc, delegates como protocols, sem tipos Swift-only na superfície). Nada de fonte Objective-C no SDK em si (é Swift), mas o consumo funciona igual nos dois idiomas.
10. Limitações declaradas do V1
- MRAID host — perfil core apenas. Mesma máquina de estados MRAID 3.0
do Android (
loading → default → expanded/resized → hidden, eventos,expand()/resize()/close(), getters), mesmometrike-mraid.jsreaproveitado, mas:mraid.getLocationestá fora por decisão de privacidade — bloqueia só o creative de terceiro de ler a localização do usuário; não afeta a medição de localização própria do SDK (§5), que não passa por MRAID.- Expand em duas partes, orientation lock fino e
audioVolumeChangeficam para V1.1.
- Sem IDFA/ATT — por decisão de privacidade, não por limitação técnica.
visitor_idé o único identificador (§5, §8). - Sem OM SDK (Open Measurement) — o SDK faz self-measurement nativo (mesmo padrão do AdButler e do Android). Certificação OM SDK para verificadores terceiros (IAS/DV) é V1.1.
contentUrlsó no banner — interstitial e os formatos de vídeo não aceitam esse parâmetro nesta versão (§3.2, §4).- Sem atribuição individual de instalação — limitação de plataforma, não deste SDK (§6, seção "Atribuição de instalação no iOS").
- tvOS fora do V1 (chega em V1.1) — diferente do Android TV (que veio de
graça no mesmo binário), tvOS é um alvo de compilação separado, sem
WKWebView— display/MRAID nem existem lá. - S2S postback, view-through, MMPs — fora de escopo (não fazem parte deste SDK nem estão planejados para V1.1 conhecido).
11. App de exemplo
Diferente do Android (que distribui um APK assinado), o deliverable de
exemplo aqui é o código-fonte — Example/ dentro do repositório público
github.com/metrikehub/metrike-ads-ios. Quem integra o SDK iOS é dev iOS
com Xcode; "clone e rode" (inclusive no Simulator) é a história do V1.
TestFlight (exige conta Apple Developer paga) fica em V1.1.
O que dá pra testar:
- Banner (320×50) e interstitial, com campo de
zoneIdeditável. - Vídeo fullscreen manual (VAST) e outstream inline, com campo de
vastTokeneditável. - Instream automático (
MetrikeAdsLoader) com um vídeo de conteúdo demo e break schedule pre/mid/post-roll sobre umAVPlayerreal. - Creative MRAID (expand/resize) — a mesma zona de banner, se configurada com um creative MRAID no painel.
- Alternância de ambiente (produção / servidor local).
- Simulação de deep link (
handleDeepLink) e de conversão (trackConversion) com um botão de exemplo. - Log de eventos na tela — todo callback e beacon aparece com timestamp, o que torna a demo autoevidente.
Zonas de teste dedicadas (org de teste id 2) — nunca uma zona real:
display 13778, vídeo 13779.