This guide covers how to add the MSP iOS SDK to your app, including how to initialize SDK, load Ad and display ads.
The guide also provides some best pratices which helps you avoid common pitfalls and achieve best performance.
System requirements
| Requirement | Version |
|---|---|
| iOS deployment target | 15.0+ |
| Xcode | 15.0+ |
| Swift | 5.0+ |
| CocoaPods | 1.12+ |
The MSP SDK is distributed via CocoaPods. Add the core pod and any adapter pods for the ad networks you want to monetize with.
Podfile
platform :ios, '15.0' target 'YourApp' do use_frameworks! # Core SDK (required) pod 'MSPCore', '{LATEST_VERSION}', :modular_headers => true # Ad network adapters (add only those you need) pod 'MSPGoogleAdapter', '{LATEST_VERSION}', :modular_headers => true pod 'MSPFacebookAdapter', '{LATEST_VERSION}', :modular_headers => true pod 'MSPNovaAdapter', '{LATEST_VERSION}', :modular_headers => true pod 'MSPMolocoAdapter', '{LATEST_VERSION}', :modular_headers => true pod 'MSPApplovinMaxAdapter', '{LATEST_VERSION}', :modular_headers => true end
Run the install command:
pod install
Open the generated .xcworkspace file — do not use the .xcodeproj directly.
Add the usage description key. The system displays this string in the ATT permission prompt:
<key>NSUserTrackingUsageDescription</key> <string>This identifier will be used to deliver personalized ads to you.</string>
Network-specific Info.plist entries (GAD identifier, SKAdNetwork IDs) are covered per-network in the Mediation Networks Guide.
Initialize the MSP SDK as early as possible — ideally in application(_:didFinishLaunchingWithOptions:). The SDK uses the initialization window to prefetch ad configurations and warm up adapter networks.
AppDelegate.swift
import AppTrackingTransparency import MSPCore import MSPiOSCore // Import adapter modules — see Mediation Networks Guide for each network's import name @main class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? // Declare adapter managers for the networks you want to activate private let adNetworkManagers: [AdNetworkManager] = [ GoogleManager(), FacebookManager(), NovaManager(), MolocoManager(), ApplovinMaxManager(), ] func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { initializeMSP() // Request ATT permission after a brief delay so your UI is visible DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { if #available(iOS 14, *) { ATTrackingManager.requestTrackingAuthorization { _ in } } } return true } private func initializeMSP() { // Optional: pass a publisher-provided user identifier for frequency capping MSP.shared.ppid = "your-user-id" // Build initialization parameters let initParams = InitializationParametersImp( prebidAPIKey: "YOUR_PREBID_API_KEY", sourceApp: "YOUR_SOURCE_APP", orgId: 12345, // Your org ID from the MSP dashboard appId: 67890 // Your app ID from the MSP dashboard ) // Additional network-specific init parameters initParams.params = [ InitializationParametersCustomKeys.MOLOCO_APP_KEY: "xxxxx-v9C8uNYV5riG7Vwu", InitializationParametersCustomKeys.APPLOVIN_SDK_KEY: "6KrA5SQHFTBpGDUU4FeLIZG...", ] // Initialize MSP.shared.initMSP( initParams: initParams, sdkInitListener: self, // optional — set to nil if not needed adNetworkManagers: adNetworkManagers ) } } // MARK: - MSPInitListener (optional) extension AppDelegate: MSPInitListener { func onComplete(status: MSPInitStatus, message: String) { print("[MSP] Initialization complete. Status: \(status), message: \(message)") } }
Key initialization parameters
| Parameter | Type | Description |
|---|---|---|
prebidAPIKey |
String |
API key issued by Particle inc. for your app |
sourceApp |
String |
App bundle identifier or source tag |
orgId |
Int64 |
Organization ID provisioned from Particle inc. |
appId |
Int64 |
App ID provisioned from Particle inc. |
params |
[String: Any]? |
Additional network-specific initialization parameters |
Optional MSP.shared properties
| Property | Default | Description |
|---|---|---|
ppid |
nil |
Publisher-provided user ID |
The pattern for every ad format is the same:
- Create an
MSPAdLoaderand anAdRequest. - Call
loadAd(placementId:adListener:adRequest:). - In
onAdLoaded, callloader.getAd(placementId:)to retrieve the ad object. - Present the Ad(for interstitial Ad) or attach the ad to the App view hierarchy.
To set a minimum price for a request, add the bid_floor custom parameter: the eCPM floor in USD, as a String ("1.25" means $1.25 per 1,000 impressions). It applies to that request only and replaces the placement's configured floor. Leave the key out to use the configured floor.
let request = AdRequest( customParams: ["bid_floor": String(format: "%.2f", 1.25)], // "1.25" geo: nil, context: nil, adaptiveBannerSize: nil, adSize: nil, placementId: "YOUR_PLACEMENT_ID", adFormat: .native ) loader.loadAd(placementId: "YOUR_PLACEMENT_ID", adListener: self, adRequest: request)
- The value must be a
String. The SDK only forwards String custom params to the ad server, so aDoublesuch as1.25is silently dropped. - The floor applies to server-side (MSP) bidding. Client-side-only placements do not use it.
Call MSP.shared.notifyLoss API when:
- MSP Ad loses the auction, or
- MSP SDK does not fill while other bidder wins
public func notifyLoss(winnerBidderName: String, winnerPrice: Float, ad: MSPAd?, requestId: String?)
winnerBidderName: name of the winning bidder other than MSPwinnderPrice: Ad price of the winning bid other than MSPad: MSP Ad that loses the auction.(passnilfor the No fill case)requestId: Provided byonAdLoadedandonErrorcallback parameterloadInfo[MSPConstants.LOAD_INFO_KEY_REQUEST_ID]ofloadAdAPI
import MSPCore import MSPiOSCore class BannerViewController: UIViewController { private let loader = MSPAdLoader() private var bannerContainerView: UIView? private func loadBanner() { let size = AdSize(width: 320, height: 50) let request = AdRequest( customParams: [:], geo: nil, context: nil, adaptiveBannerSize: nil, adSize: size, placementId: "YOUR_BANNER_PLACEMENT_ID", adFormat: .banner ) loader.loadAd(placementId: "YOUR_BANNER_PLACEMENT_ID", adListener: self, adRequest: request) } } extension BannerViewController: AdListener { func onAdLoaded(placementId: String, loadInfo: [String: Any]) { guard let bannerAd = loader.getAd(placementId: placementId) as? BannerAd else { return } let adView = bannerAd.adView adView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(adView) NSLayoutConstraint.activate([ adView.centerXAnchor.constraint(equalTo: view.centerXAnchor), adView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), adView.widthAnchor.constraint(equalToConstant: 320), adView.heightAnchor.constraint(equalToConstant: 50), ]) } func onError(msg: String, loadInfo: [String: Any]) { print("[MSP] Banner error: \(msg)") } func onAdImpression(ad: MSPAd) {} func onAdClick(ad: MSPAd) {} func onAdDismissed(ad: MSPAd) {} func getRootViewController() -> UIViewController? { self } }
Standard banner sizes
| Size | Width | Height |
|---|---|---|
| Banner | 320 | 50 |
| Medium rectangle | 300 | 250 |
| Leaderboard | 728 | 90 |
Load the interstitial before you need to show it. Display it at a natural transition point — level completion, article end, etc.
import MSPCore import MSPiOSCore class InterstitialViewController: UIViewController { private let loader = MSPAdLoader() private func loadInterstitial() { let request = AdRequest( customParams: [:], geo: nil, context: nil, adaptiveBannerSize: nil, adSize: nil, placementId: "YOUR_INTERSTITIAL_PLACEMENT_ID", adFormat: .interstitial ) loader.loadAd(placementId: "YOUR_INTERSTITIAL_PLACEMENT_ID", adListener: self, adRequest: request) } private func showInterstitialIfReady() { guard let interstitialAd = loader.getAd(placementId: "YOUR_INTERSTITIAL_PLACEMENT_ID") as? InterstitialAd else { loadInterstitial() // not ready yet — start loading return } interstitialAd.show(rootViewController: self) } } extension InterstitialViewController: AdListener { func onAdLoaded(placementId: String, loadInfo: [String: Any]) { print("[MSP] Interstitial ready") showInterstitialIfReady() } func onError(msg: String, loadInfo: [String: Any]) { print("[MSP] Interstitial error: \(msg)") } func onAdDismissed(ad: MSPAd) { print("[MSP] Interstitial Ads is dismissed: \(msg)") } func onAdImpression(ad: MSPAd) {} func onAdClick(ad: MSPAd) {} func getRootViewController() -> UIViewController? { self } }
Rewarded ads are full-screen placements that grant a reward — coins, lives, premium content access — after the user completes the ad experience. The reward is delivered via onAdRewardReceived.
import MSPCore import MSPiOSCore class RewardedViewController: UIViewController { private let loader = MSPAdLoader() private func loadRewardedAd() { let request = AdRequest( customParams: [:], geo: nil, context: nil, adaptiveBannerSize: nil, adSize: nil, placementId: "YOUR_REWARDED_PLACEMENT_ID", adFormat: .rewarded ) loader.loadAd(placementId: "YOUR_REWARDED_PLACEMENT_ID", adListener: self, adRequest: request) } @IBAction func userTappedWatchAd(_ sender: UIButton) { guard let rewardedAd = loader.getAd(placementId: "YOUR_REWARDED_PLACEMENT_ID") as? RewardedAd else { print("[MSP] Rewarded ad not ready yet") return } rewardedAd.show(rootViewController: self) } } extension RewardedViewController: AdListener { // Override this to grant the reward to the user func onAdRewardReceived(ad: MSPAd) { guard let rewardedAd = ad as? RewardedAd else { return } let reward = rewardedAd.reward print("[MSP] Reward: \(reward.amount) \(reward.type)") // Grant the reward to the user here } func onAdLoaded(placementId: String, loadInfo: [String: Any]) {} func onError(msg: String, loadInfo: [String: Any]) {} func onAdDismissed(ad: MSPAd) {} func onAdImpression(ad: MSPAd) {} func onAdClick(ad: MSPAd) {} func getRootViewController() -> UIViewController? { self } }
Native ads return a NativeAd object you render with your own layout. Implement MSPNativeAdContainer to wire up your UI elements, then pass it to NativeAdView.
import MSPCore import MSPiOSCore class NativeViewController: UIViewController { private let loader = MSPAdLoader() private func loadNativeAd() { let request = AdRequest( customParams: [:], geo: nil, context: nil, adaptiveBannerSize: nil, adSize: nil, placementId: "YOUR_NATIVE_PLACEMENT_ID", adFormat: .native ) loader.loadAd(placementId: "YOUR_NATIVE_PLACEMENT_ID", adListener: self, adRequest: request) } } extension NativeViewController: AdListener { func onAdLoaded(placementId: String, loadInfo: [String: Any]) { guard let nativeAd = loader.getAd(placementId: placementId) as? NativeAd else { return } DispatchQueue.main.async { let container = MyNativeAdContainer(frame: CGRect(x: 0, y: 0, width: 300, height: 250)) let nativeAdView = NativeAdView(nativeAd: nativeAd, nativeAdContainer: container) nativeAdView.translatesAutoresizingMaskIntoConstraints = false self.view.addSubview(nativeAdView) NSLayoutConstraint.activate([ nativeAdView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor), nativeAdView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor), nativeAdView.widthAnchor.constraint(equalToConstant: 300), ]) } } func onError(msg: String, loadInfo: [String: Any]) {} func onAdImpression(ad: MSPAd) {} func onAdClick(ad: MSPAd) {} func onAdDismissed(ad: MSPAd) {} func getRootViewController() -> UIViewController? { self } }
Implement MSPNativeAdContainer to provide the UI elements the SDK will populate:
class MyNativeAdContainer: UIView, MSPNativeAdContainer { private let titleLabel = UILabel() private let bodyLabel = UILabel() private let advertiserLabel = UILabel() private let ctaButton = UIButton() private let mediaView = UIView() private let iconView = UIImageView() func getTitle() -> UILabel? { titleLabel } func getbody() -> UILabel? { bodyLabel } func getAdvertiser() -> UILabel? { advertiserLabel } func getCallToAction() -> UIButton? { ctaButton } func getMedia() -> UIView? { mediaView } func getIcon() -> UIImageView? { iconView } func getCustomClickableViews() -> [UIView]? { nil } }
.multi_format allows both native and banner ads to fill the same placement. In onAdLoaded, check which type was returned and render accordingly.
import MSPCore import MSPiOSCore class MultiFormatViewController: UIViewController { private let loader = MSPAdLoader() private func loadAd() { let request = AdRequest( customParams: [:], geo: nil, context: nil, adaptiveBannerSize: nil, adSize: nil, placementId: "YOUR_MULTI_FORMAT_PLACEMENT_ID", adFormat: .multi_format ) loader.loadAd(placementId: "YOUR_MULTI_FORMAT_PLACEMENT_ID", adListener: self, adRequest: request) } } extension MultiFormatViewController: AdListener { func onAdLoaded(placementId: String, loadInfo: [String: Any]) { let ad = loader.getAd(placementId: placementId) DispatchQueue.main.async { if let nativeAd = ad as? NativeAd { let container = MyNativeAdContainer(frame: CGRect(x: 0, y: 0, width: 300, height: 250)) let nativeAdView = NativeAdView(nativeAd: nativeAd, nativeAdContainer: container) nativeAdView.translatesAutoresizingMaskIntoConstraints = false self.view.addSubview(nativeAdView) NSLayoutConstraint.activate([ nativeAdView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor), nativeAdView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor), nativeAdView.widthAnchor.constraint(equalToConstant: 300), ]) } else if let bannerAd = ad as? BannerAd { let adView = bannerAd.adView adView.translatesAutoresizingMaskIntoConstraints = false self.view.addSubview(adView) NSLayoutConstraint.activate([ adView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor), adView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor), adView.widthAnchor.constraint(equalToConstant: 320), adView.heightAnchor.constraint(equalToConstant: 50), ]) } } } func onError(msg: String, loadInfo: [String: Any]) {} func onAdImpression(ad: MSPAd) {} func onAdClick(ad: MSPAd) {} func onAdDismissed(ad: MSPAd) {} func getRootViewController() -> UIViewController? { self } }
Network-specific Info.plist entries required for each adapter are listed below.
| Pod | Ad Network | Formats |
|---|---|---|
MSPGoogleAdapter |
Google Ad Manager / AdMob | Banner, Interstitial, Rewarded, Native |
MSPFacebookAdapter |
Meta Audience Network | Banner, Interstitial, Rewarded, Native |
MSPNovaAdapter |
Nova | Banner, Interstitial, Native, Rewarded |
MSPMolocoAdapter |
Moloco | Banner, Native, Interstitial, Rewarded |
MSPApplovinMaxAdapter |
AppLovin/Max | Banner, Native, Interstitial, Rewarded |
Info.plist — GAD identifier
Add your app's GAD identifier from the Google AdMob dashboard:
<key>GADApplicationIdentifier</key> <string>ca-app-pub-XXXXXXXXXXXXXXXX~XXXXXXXXXX</string>
Info.plist — SKAdNetwork
<dict> <key>SKAdNetworkIdentifier</key> <string>cstr6suwn9.skadnetwork</string> </dict>
Info.plist — SKAdNetwork
<dict> <key>SKAdNetworkIdentifier</key> <string>v9wttpbfk9.skadnetwork</string> </dict> <dict> <key>SKAdNetworkIdentifier</key> <string>n38lu8286q.skadnetwork</string> </dict>
Pass your Moloco App Key via params during initialization:
initParams.params = [ InitializationParametersCustomKeys.MOLOCO_APP_KEY: "YOUR_MOLOCO_APP_KEY" ]
Pass your AppLovin SDK Key via params during initialization:
initParams.params = [ InitializationParametersCustomKeys.APPLOVIN_SDK_KEY: "6KrA5SQHFTBpGDUU4FeLIZG..." ]
The auction takes time. Suggest Call loadAd before the moment you need to show the ad:
- Banners: Load when the view controller loads (
viewDidLoad). - Interstitials: Load at app launch or immediately after the previous interstitial is dismissed.
- Rewarded: Load at app launch and again in
onAdDismissed.
When an ad is no longer needed, call destroy() to release adapter resources and prevent memory leaks:
override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) currentAd?.destroy() currentAd = nil }
Keep a strong reference to your MSPAdLoader instance for the lifetime of the placement. The AdListener is held weakly by the loader — your view controller or delegate object must remain alive until onAdLoaded or onError fires.
No ads filling
- Verify the placement IDs match what is Provisioned by Particles inc.
- Confirm
initMSPcompletes beforeloadAdis called. - Enable verbose logging during development:
MSPLogger.shared.setLogLevel(level: MSPLogger.DEBUG).
onAdLoaded fires but getAd returns nil
getAd removes the ad from cache. Call it exactly once per load cycle and hold the returned ad strongly until you are done with it.
ATT prompt not appearing
ATTrackingManager.requestTrackingAuthorization must be called from the main thread after your root view controller is visible. The 1-second delay in the sample above handles the most common case; adjust as needed for your presentation flow.
Rewarded ad shows but reward is never granted
onAdRewardReceived fires only if the user watches the ad to completion. Do not grant rewards based on onAdDismissed alone.
Prebid SDK which is introduced by by MSP SDK will read UserDefaults (iOS) or SharedPreferences (Android) key IABUSPrivacy_String for US Privacy signal. Prebid SDK CCPA doc
Prebid SDK which is introduced by by MSP SDK will read UserDefaults (iOS) or SharedPreferences (Android) keys IABTCF_gdprApplies and IABTCF_TCString for GDPR privacy signals. Predbid SDK GDPR doc
Set your COPPA signal through init parameter isAgeRestrictedUser