Skip to content

Nitx Publisher SDK

iOS SDK

Swift package with native rendering, App Tracking Transparency support and a privacy manifest.

Swift
NitxPublisher
iOS 15+
Swift 5.9
No dependencies
Updated Nov 1, 2026

Swift Package NitxPublisher, the iOS implementation of the Nitx Publisher SDK (Protocol §10 of the web SDK repository). It serves banner, interstitial, app open and native ads from a Nitx mobile source, with the same visitor, consent, identify and hashing rules as the web SDK 1.2.

  • iOS 15+, Swift 5.9, no third-party dependencies (Foundation, UIKit, CryptoKit, AVFoundation, SafariServices, AdSupport, AppTrackingTransparency).
  • Image and video creatives render natively; clicks open the backend click URL in SFSafariViewController.
  • Every public call is safe from any thread and never throws into the app. Callbacks arrive on the main thread.

Install (Swift Package Manager)

In Xcode: File > Add Package Dependencies, enter the repository URL and add the NitxPublisher product to your app target. Or in Package.swift:

Swift
dependencies: [
    .package(url: "https://github.com/NitxTech/nitx-publisher-sdk-ios.git", from: "1.0.0")
],
targets: [
    .target(name: "MyApp", dependencies: [.product(name: "NitxPublisher", package: "nitx-publisher-sdk-ios")])
]

Start

Call Nitx.start once, as early as possible (app delegate or @main App init). The code is the mobile source code shown in the publisher dashboard (SDK-AM-XXXXX).

Swift
import NitxPublisher

Nitx.start(code: "SDK-AM-XXXXX")

// Options
Nitx.start(code: "SDK-AM-XXXXX", options: NitxOptions(
    api: "https://api.nitx.app",   // base URL override
    debug: true,                   // unified log, subsystem io.nitx.publisher
    personalization: true,         // false: no storage, no visitor block, consent/identify/reset are no-ops
    bridge: nil                    // set by the React Native and Flutter bridges ("rn-1.0.0"); context.sdk then reads rn-1.0.0/ios-1.0.0
))

Nitx.setScreen("HomeFeed")         // optional, sent as context.app.screen with the next requests
let state = Nitx.state()            // counters, mode, consent, visitor id, profile keys

The SDK loads GET /api/ad-mobiles/show/{code}, warns when the source's bundle_id differs from the app's bundle identifier, and calls GET /api/ad-mobiles/init/{code} once per process while the source is pending_verification.

Formats

UIKit: NitxBannerView loads when it enters a window, load() reloads. Give it the slot's size (adaptive banners take the full width and a height of 50 to 90 points).

Swift
let banner = NitxBannerView(slot: "SLOT-XXXXXX")
banner.delegate = self   // bannerDidLoad, banner(_:didFailWith:), bannerDidRecordImpression, bannerDidClick
view.addSubview(banner)
banner.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
    banner.widthAnchor.constraint(equalToConstant: 320),
    banner.heightAnchor.constraint(equalToConstant: 50),
    banner.centerXAnchor.constraint(equalTo: view.centerXAnchor),
    banner.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor)
])

SwiftUI:

Swift
NitxBanner(slot: "SLOT-XXXXXX") { event in
    if case .failed(let error) = event { print(error) }
}
.frame(width: 320, height: 50)

Interstitial (interstitial)

Swift
NitxInterstitial.load(slot: "SLOT-XXXXXX") { result in
    switch result {
    case .success(let interstitial):
        interstitial.delegate = self     // adDidShow, adDidClick, adDidDismiss, ad(_:didFailToShow:)
        self.interstitial = interstitial
        if interstitial.isReady { interstitial.show(from: self) }
    case .failure(let error):
        print(error)
    }
}

The ad is preloaded (video creatives are downloaded first), presented full screen, and gets a close control after at most 5 seconds and on video end. Shown counts as both impression and viewable.

App open (app_open)

Swift
// Manual
NitxAppOpen.load(slot: "SLOT-XXXXXX")
NitxAppOpen.showIfAvailable(from: rootViewController, minInterval: 4 * 3600)

// Automatic: preloads and shows on every foreground return, at most once per interval (seconds)
NitxAppOpen.enableAutoShow(slot: "SLOT-XXXXXX", minInterval: 4 * 3600)
NitxAppOpen.delegate = self

The last show time is kept in UserDefaults under nitx.app_open_last_shown.

Native (native)

The SDK supplies assets; the app owns the layout. Register the container so the SDK can track viewability and attach click handling to the views you choose.

Swift
NitxNativeAd.load(slot: "SLOT-XXXXXX") { result in
    guard case .success(let ad) = result else { return }
    let assets = ad.assets   // headline, body, cta, advertiser, icon, image, video, rating, price (all optional)
    cell.headline.text = assets.headline
    cell.body.text = assets.body
    cell.cta.setTitle(assets.cta ?? "Learn more", for: .normal)
    if let url = assets.image { cell.loadImage(url) }
    ad.delegate = self   // nativeAdDidRecordImpression, nativeAdDidClick
    ad.registerView(cell.contentView, clickableViews: [cell.contentView, cell.cta])
}
// later
ad.destroy()

Until the backend ships structured assets, a path-only ad arrives as headline = ad name and image = creative path.

Hosts that draw the ad outside UIKit (Flutter, game engines) can register a transparent view behind their rendering for viewability and forward their own tap with ad.performClick(). It records the click and opens the destination exactly like a tap on a registered clickable view, and does nothing before registerView, after destroy(), or when the ad has no click URL.

SwiftUI: NitxNativeAdLoader is an ObservableObject with load(slot:), ad, error and isLoading. Put .background(NitxNativeAdRegistration(ad: ad)) on the view that renders the assets so the SDK tracks it.

Swift
struct FeedAd: View {
    @StateObject private var loader = NitxNativeAdLoader()
    var body: some View {
        Group {
            if let ad = loader.ad {
                VStack(alignment: .leading) {
                    Text(ad.assets.headline ?? "Sponsored").bold()
                    if let body = ad.assets.body { Text(body) }
                }
                .background(NitxNativeAdRegistration(ad: ad))
            }
        }
        .onAppear { loader.load(slot: "SLOT-XXXXXX") }
    }
}

Rewarded (rewarded)

NitxRewarded.load(slot:) exists so the API is stable, and always fails with "Rewarded ads are not available in v1".

The SDK never reads your forms, cookies or contacts. Identity enters only through identify, and identity fields leave the device only after consent(true).

Swift
// From your own consent banner
Nitx.consent(true)     // stores a consent record, persists the buffered profile
Nitx.consent(false)    // deletes the profile and the consent record; the visitor id stays

// Normalised and hashed (SHA-256 via CryptoKit) immediately; raw values are discarded
Nitx.identify(NitxTraits(email: "Jane@Example.com", phone: "+966 50 123 4567", gender: "f", birthYear: 1990,
                         city: "Riyadh", country: "SA", traits: ["plan": "pro"]))

// Delete one field
var traits = NitxTraits()
traits.remove(.email)
Nitx.identify(traits)

// Forget the visitor (id, profile, consent, cached ad id), best-effort forget request when advertised
Nitx.reset()

Rules: email trimmed and lowercased, phone reduced to E.164 (+ and 8 to 15 digits, numbers without a country code are dropped), gender m, f or other, birth year 1900 to the current year (or derived from age), city and country lowercased and cut at 64 characters, traits at most 20 keys of [a-z0-9_]{1,32}. Calls implying an age under 16 are discarded.

Storage (UserDefaults, prefix nitx.): visitor_id, consent, profile (hashes only, persisted only with a consent record and discarded on load without one), app_open_last_shown.

Request mode

Today the SDK reads creatives with GET /api/ad-slots/show/{code} per slot and sends no tracking (no impression ids exist). When the config response carries the sdk block with relative ads_endpoint, events_endpoint and forget_endpoint paths, the SDK switches to POST ads with the visitor and app context blocks, queues impression and viewable events (batched every 2 seconds, flushed when the app resigns active or enters the background), and sends forget on reset. Off-origin paths are ignored, and a 404, 405 or 501 from the POST endpoint falls back to GET for the rest of the process. NitxOptions.ads (.auto, .get, .post) is a developer override.

The context block contains the bundle id, app version and build, the screen name, OS name and version, phone or tablet, language, time zone, screen size and the SDK tag ios-1.0.0. The SDK never sends the IP address, user agent or location.

App Tracking Transparency

The SDK never shows the ATT prompt. It reads ATTrackingManager.trackingAuthorizationStatus at start and after consent changes, and includes the IDFA in context.ad_id only when the status is .authorized. If you want the IDFA on the wire, request authorization yourself (with NSUserTrackingUsageDescription in your Info.plist) before Nitx.start or before the first ad request. Without it the block is null and everything still works.

Privacy manifest

The package ships Sources/NitxPublisher/PrivacyInfo.xcprivacy, declaring: email address and phone number (hashed, optional, with consent, linked to the user, advertising), coarse location (derived from the IP on the server), device id (optional, advertising), product interaction (ad impressions and clicks), and the UserDefaults required-reason API (CA92.1). Tracking is declared as false and no tracking domains are listed. Merge these entries into your own app privacy answers on App Store Connect.

Contributing

The sections below are for working on the SDK itself: running it locally, testing and releasing. You don't need them to integrate.

Local testing against the Mac backend

The sample app in Sample/ is generated with XcodeGen:

Shell
brew install xcodegen
cd Sample && xcodegen generate
open Sample.xcodeproj

It starts with code SDK-AM-DEMOI, API http://127.0.0.1:8001 and debug: true, and shows a 320x50 banner, a 300x250 rectangle, a feed with a native ad every third row, interstitial and app open buttons, a consent switch, identify fields, a reset button and the Nitx.state() output. The simulator reaches the Mac's localhost directly, so run the backend on port 8001 and the creative server on port 4300 as for the web SDK. NSAllowsArbitraryLoads is on in the sample only.

Launch argument -nitxAutoShowInterstitial 1 makes the sample show the interstitial 3 seconds after launch (handy for automated screenshots).

Logs: xcrun simctl spawn booted log stream --predicate 'subsystem == "io.nitx.publisher"'.

Run the package tests on a simulator:

Shell
xcodebuild -scheme NitxPublisher -destination 'platform=iOS Simulator,name=iPhone 16' test

Note: Xcode needs an iOS simulator runtime matching its SDK major version installed (Xcode > Settings > Components, or xcodebuild -downloadPlatform iOS), otherwise it reports "iOS X is not installed" and refuses every simulator destination. Without it you can still compile for the simulator with swift build --triple x86_64-apple-ios15.0-simulator --sdk $(xcrun --sdk iphonesimulator --show-sdk-path) (use arm64-apple-ios15.0-simulator on Apple silicon).

Deviations from Protocol §10 in v1

  • html creatives are not rendered (no WebKit dependency); they fail with creativeFailed.
  • Video creatives are downloaded to the caches directory before playback instead of streamed.
  • Rewarded ads are a stub.

Generated from README.md in NitxTech/nitx-publisher-sdk-ios at 49554b4 · Nov 1, 2026