Campaigns

Campaigns are the core mechanism the SDK uses to respond to application events. When you fire an event, the SDK evaluates all configured campaigns and returns the best matching one, if any is available.

Before requesting campaigns, make sure you have configured and initialized MagifyClient and called setup().

Requesting a campaign for an event

Call campaign(for:parameters:silent:) at any point in the app where you want to trigger campaign evaluation. The method returns a Campaign?nil means no campaign matched the event under current conditions.

// Basic request — triggers impression tracking and conversion events
if let campaign = magify.campaign(for: "level_finished") {
    // handle campaign
}

// With optional parameters
let params: [String: Any] = ["difficulty": "hard", "score": 1500]
if let campaign = magify.campaign(for: "level_finished", parameters: params) {
    // handle campaign
}

// Silent request — evaluates availability without counting an impression or firing analytics
if let campaign = magify.campaign(for: "level_finished", silent: true) {
    // pre-check before showing UI
}

The @discardableResult annotation means you can also call the method purely for its side effects (impression tracking) without capturing the return value.

Checking availability without triggering a campaign

Use isCampaignAvailableNow(named:forEvent:parameters:) to test whether a specific named campaign would fire for an event. This does not count as an impression.

let isAvailable = magify.isCampaignAvailableNow(
    named: "summer_sale",
    forEvent: "shop_opened"
)

if isAvailable {
    // show a teaser or badge in the UI
}

Retrieving the last shown campaign

lastCampaign(for:) returns the most recently returned campaign of a given type, without issuing a new request.

if let last = magify.lastCampaign(for: .subscription) {
    // display the same subscription offer that was shown earlier
}

Campaign types

CampaignType is a String-backed enum. The values that map to concrete campaign objects are:

LTO variants (see Limited-Time Offers below):

The special .all case is used for global queries, not returned as a campaign type in responses.

Campaign is an enum with associated values. Each case carries a CampaignSource (name, type, triggering event, parameters) plus type-specific payloads:

switch campaign {
case let .subscription(source, screen, products):
    // present subscription paywall
case let .inApp(source, screen, products):
    // present in-app purchase screen
case let .interstitial(source, splashscreen, attributes):
    // show interstitial ad
case let .rewardedVideo(source, screen, products):
    // start rewarded ad flow
case let .bonus(source, screen, products):
    // grant free reward
case let .banner(source, position):
    // show banner at .top or .bottom
case let .promo(source, destination, screen):
    // navigate to promoted app or external link
case let .mixed(source, screen, products):
    // mixed campaign
case .rateReview(let source):
    // prompt for App Store rating
case let .notification(source, screen):
    // handle notification campaign
}

Access common properties via extensions:

let name = campaign.name        // String — campaign name from the dashboard
let type = campaign.type        // CampaignType
let event = campaign.source.event  // the event that triggered this campaign

Reacting to campaign updates

Register a CampaignChangeDelegate to be notified whenever the configuration for a specific named campaign changes (e.g., after a remote config refresh or a status change that makes a different variant eligible).

class MyPresenter: CampaignChangeDelegate {
    func setup(magify: MagifyClient) {
        magify.setupCampaignChangeDelegate(self, campaignName: "summer_sale")
    }

    // Called when the campaign's resolved output changes
    func campaignChanged(_ campaign: Campaign) {
        // update your UI to reflect the new campaign variant
    }
}

Remove the observer when it is no longer needed:

magify.resetCampaignChangeDelegate(forCampaignName: "summer_sale")

Each campaign name can have at most one registered delegate. Calling setupCampaignChangeDelegate(_:campaignName:) again with the same name replaces the previous one.

Limits

MagifyClient.limits exposes the current server-side throttling configuration as a Limits value. Read it to understand how many impressions the SDK allows per session, day, or globally.

let limits = magify.limits

// Global impression interval between any two campaigns
if let globalInterval = limits.globalInterval {
    print("Min seconds between campaigns: \(globalInterval)")
}

// Per-type caps
if let inAppCaps = limits.inAppsLimits {
    print("In-app per session: \(inAppCaps.session ?? -1)")
    print("In-app per day:     \(inAppCaps.day ?? -1)")
    print("In-app global:      \(inAppCaps.global ?? -1)")
}

Limits fields (all optional — nil means no limit configured):

CampaignTypeLimits has three optional integer fields: session, day, global.

Limited-Time Offers

Limited-Time Offers (LTO) are time-bounded campaigns with their own lifecycle. The SDK manages the LTO state internally — your app receives callbacks when offers are added, updated, or completed.

Setting up the LTO delegate

Implement LtoCampaignManagerDelegate and register it once, typically at app startup:

class AppDelegate: UIResponder, UIApplicationDelegate, LtoCampaignManagerDelegate {

    var magify: MagifyClient!

    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // ... MagifyClient initialization ...
        magify.setupLtoWith(delegate: self)
        return true
    }

    // Called when the set of active offers changes (offer added or removed)
    func ltoCampaignManagerDidUpdate(activeOffers: Set<LtoInfo>) {
        // Refresh any LTO badge or banner UI
    }

    // Called when an offer's timer expires or the offer is completed programmatically
    func ltoCampaignManagerDidFinish(offer: LtoInfo) {
        // Hide the corresponding UI element
    }
}

Reading active offers

activeLtoOffers is a Set<LtoInfo> containing all currently active offers. Each LtoInfo describes one active LTO:

for offer in magify.activeLtoOffers {
    print("Campaign: \(offer.campaignName)")
    print("Spot:     \(offer.spot)")           // UI placement identifier
    print("Start:    \(offer.startTime)")       // Unix timestamp (seconds)
    print("End:      \(offer.endTime)")         // Derived end time (seconds)
    print("Duration: \(offer.durationMinutes) min")
}

Key LtoInfo properties:

| durationMinutes | Int | Configured duration in minutes | | badgeCreative | BadgeCreative | Visual badge configuration (image/Lottie, title, label, timer flag) | | defaultSpotImage | String? | Fallback image URL for the spot |

Completing an offer

Call completeLto(withCampaignName:) when the user converts (purchases the associated product, redeems the reward, etc.). This ends the offer and triggers ltoCampaignManagerDidFinish(offer:).

// After a successful purchase tied to the LTO
magify.completeLto(withCampaignName: "summer_sale")

Presenting campaigns

After campaign(for:) returns a non-nil Campaign, use the matching presenter class to display it. Each presenter manages a single in-flight presentation and calls back through CampaignPresenterDelegate.

CampaignPresenterDelegate

Implement this protocol to receive lifecycle events from any presenter. Some methods have default no-op implementations (campaignWillPresent, campaignDidDismiss, impressionFailed, clickDone, and the no-argument impressionFired(for:)), but the following are required and must be implemented: impressionFired(for:info:), adsImpressionFired(for:impressionData:), adsClickDone(for:productID:), and adsPresenterDidPayRevenue(revenueDouble:).

class MyPresenterHost: CampaignPresenterDelegate {

    // Called just before the creative view appears (optional — has default)
    func campaignWillPresent(for campaignType: CampaignType) { }

    // Called after the creative is dismissed (optional — has default)
    func campaignDidDismiss(for campaignType: CampaignType) { }

    // Required — called when the impression is counted (view did appear)
    func impressionFired(for campaignType: CampaignType, info: [String: Any]?) { }

    // Required — called when an ad impression is fired for an ad campaign
    func adsImpressionFired(for campaignType: CampaignType, impressionData: AppLovinImpressionData) { }

    // Called when the impression could not be shown (optional — has default)
    func impressionFailed(for campaignType: CampaignType, with reason: String) { }

    // Called when the user taps the action button (optional — has default)
    func clickDone(for campaignType: CampaignType) { }

    // Required — called when the user taps an ad campaign product
    func adsClickDone(for campaignType: CampaignType, productID: String?) { }

    // Required — called when an ad pays revenue
    func adsPresenterDidPayRevenue(revenueDouble: Double) { }
}

CreativePresenterFailReason defines the two string values passed to impressionFailed:

InAppCampaignPresenter

Use for .inApp and .bonus campaigns. BonusCampaignPresenter is a subclass with identical API.

let presenter = InAppCampaignPresenter()
presenter.delegate = self  // CampaignPresenterDelegate

if let campaign = magify.campaign(for: "shop_opened") {
    presenter.present(
        campaign: campaign,
        in: self,                      // UIViewController
        onFailToPresent: {
            // no creative could be displayed
        },
        tapHandler: { wantToBuy, product in
            if wantToBuy {
                // initiate purchase with product.productID
            }
        }
    )
}

Key properties:

To close the current creative programmatically:

presenter.closeCurrent { didClose in
    // didClose is false if nothing was presented
}

SubscriptionCampaignPresenter

Identical API to InAppCampaignPresenter; the tapHandler product type is InAppProduct.

let presenter = SubscriptionCampaignPresenter()
presenter.delegate = self

if let campaign = magify.campaign(for: "paywall_trigger") {
    presenter.present(
        campaign: campaign,
        in: self,
        onFailToPresent: { },
        tapHandler: { wantToBuy, product in
            if wantToBuy {
                // initiate subscription purchase with product.productID
            }
        }
    )
}

MixedCampaignPresenter

Use for .mixed campaigns. The tapHandler delivers a MixedProduct.

let presenter = MixedCampaignPresenter()
presenter.delegate = self

if let campaign = magify.campaign(for: "mixed_offer") {
    presenter.present(
        campaign: campaign,
        in: self,
        onFailToPresent: { },
        tapHandler: { wantToGet, product in
            // product is MixedProduct
        }
    )
}

RewardedCampaignPresenter

Use for .rewardedVideo campaigns. Provide the screen and products arrays extracted from the campaign's associated values.

let presenter = RewardedCampaignPresenter()
presenter.delegate = self

if case let .rewardedVideo(source, screen, products) = campaign {
    presenter.presentRewardedCreative(
        campaign: campaign,
        screen: screen,
        products: products,
        in: self,
        onFailToPresent: { },
        tapHandler: { wantsToGet, product in
            if wantsToGet {
                // grant the reward for product
            }
        }
    )
}

PromoCampaignPresenter

Use for .promo (cross-promo / external link) campaigns. The tapHandler receives a PromoDestination and a Bool indicating whether the user tapped the action (true) or closed (false).

let presenter = PromoCampaignPresenter()
presenter.delegate = self

if let campaign = magify.campaign(for: "cross_promo_spot") {
    presenter.present(
        campaign: campaign,
        in: self,
        timeout: 5.0,
        onFailToPresent: { },
        tapHandler: { didTapAction, destination in
            if didTapAction {
                // open destination.url or navigate to destination.appStoreID
            }
        }
    )
}

NotificationAlertPresenter

Use for .notification campaigns. The creative renders as a system-style alert overlay rather than a modal sheet. Only one notification can be on screen at a time — if you call present while one is already showing, the current one is dismissed first.

let presenter = NotificationAlertPresenter()
presenter.delegate = self

if let campaign = magify.campaign(for: "notification_spot") {
    presenter.present(
        campaign: campaign,
        in: self,
        onFailToPresent: { },
        tapHandler: { tappedAction in
            // tappedAction == true: user tapped the action button
            // tappedAction == false: user dismissed
        }
    )
}

To dismiss programmatically:

presenter.hidePresentedNotificationCampaign(animated: true, completion: nil)

Rendering LTO badge creatives

LtoBadgePresenter is a static helper — no instance needed. Call it from the view that hosts your LTO badge UI.

// imageView and animationView are your UIImageView / LottieAnimationView outlets
LtoBadgePresenter.presentBadgeResource(
    imageView: badgeImageView,
    animationView: badgeLottieView,        // must be a LottieAnimationView
    resource: offer.badgeCreative.resource, // CreativeResourceType
    placeholder: offer.defaultSpotImage,
    completion: { success in
        // success == false if the resource type is not supported (e.g. .bundle)
    }
)

CreativeResourceType has three cases: .image(URL), .lottie(URL), and .compressedLottie(URL). The .bundle case is not supported in the native SDK and always calls the completion with false.

Rendering product creatives

ProductCreativePresenter works the same way as LtoBadgePresenter but is intended for product-level creatives (e.g., offer imagery rendered inside a custom paywall).

ProductCreativePresenter.presentResource(
    imageView: productImageView,
    animationView: productLottieView,
    resource: creative.resource,
    placeholder: nil,
    completion: { success in }
)

Next step

For tracking campaign impressions and clicks, see the Analytics section.

Related articles

LimitedTimeOfferBase

Configuration options

IServicePrefs

PeriodType

TrustedPurchaseRecord

Utils