Documentation Index

Fetch the complete documentation index at: https://academy.insiderone.com/llms.txt

Use this file to discover all available pages before exploring further.

Configure App Frames

Prev Next

Push notifications and in-app messages are powerful, but they are ephemeral or interruptive: a push disappears once it is tapped or dismissed, and an in-app message overlays whatever the user is doing. App Frames takes a different approach. It renders marketer-managed content inline, as a natural part of your screen, like a banner on the home page, a contextual offer on a product detail, a persistent announcement slot, rather than an overlay on top of it.

Your development team wires it once, enables the feature in the manifest, places an InsiderAppFramesView in the layout, and assigns it a placement ID. From then on, you can create, target, and update the content in that placement entirely from Insider One's Action Builder, with no app release when the content changes.

App Frames are designed to fail silently. When a placement has no active campaign or its content cannot be loaded, the view renders nothing and measures to zero height. Your app keeps working normally, with no visible error and no empty box. Design your layout so a collapsed frame looks natural.

Your development team defines placements once during SDK integration, naming slots such as home_page or product_detail_bottom where inline content can appear. Then, you can create and target campaigns against those placements from Action Builder. Each InsiderAppFramesView you embed automatically fetches and renders the campaign currently targeting its placement while it is attached to a window; there is no manual load call to make.

Capabilities

  • Automatic refresh: The SDK refreshes App Frames content when the app comes to the foreground and when the user's identity changes, for example after login or logout. Transient network failures during the placements fetch are automatically retried with backoff.

  • In-memory caching: Rendered content is cached in memory for the lifetime of the app process, so returning to a screen re-renders instantly without a new fetch. Nothing is persisted to disk.

  • Automatic analytics: Impressions, clicks, and dismissals are logged by the SDK. There is no analytics API to call and nothing extra to track. An impression is recorded at the moment the template signals its content is on screen, and the frame has been laid out with a non-zero height, not merely when loading finishes off screen. If a new session opens while the frame is still on screen, a further impression is recorded for that session.

  • Marketer-side control: Marketers design content and target audiences, and run up to 8 variants per campaign from Action Builder, with campaign priority determining which variant wins placement.

With App Frames, you can create:

  • A home-screen promotional banner that rotates campaigns without shipping an update

  • A contextual offer on a product-detail screen, targeted to the user viewing it

  • A persistent announcement slot that marketers switch on and off as needed

  • A seasonal campaign area that fills with content during a promotion and collapses the rest of the year

Requirements

Before getting started, it is important to understand the technical expectations for integrating App Frames.

  • Opt-in is required. App Frames stays dormant until you enable it with the App Frames Enabled key inside the Insider dictionary of your app's Info.plist. Refer to Enable App Frames. Without it, the SDK never resolves placements, nothing renders, and no error is reported.

  • Fail-silent contract. A placement with no active campaign, or one whose content cannot be loaded, renders nothing and collapses to a height of 0. Do not reserve space for a frame that might be empty

  • The SDK never hides or removes the view for you. It renders content and reports events; your app owns the view's visibility and its dismissal.

  • Main-actor isolated. InsiderAppFramesView and InsiderAppFramesViewDelegate are annotated NS_SWIFT_UI_ACTOR, so in Swift they are @MainActor-isolated, and the compiler enforces it. Create, configure, and touch the view, and handle every delegate callback on the main thread.

  • The class cannot be subclassed (objc_subclassing_restricted). Embed it and drive it through placementId and delegate; wrap it in a container view of your own if you need to extend its behavior.

  • App Frames ships inside the core InsiderMobile module. No extra module, dependency, or scheme to add is available.

  • The minimum iOS deployment target is 11.0, the same as the SDK.

  • The Insider SDK must be integrated and initialized in your app before App Frames content can render.

  • User consent must be granted. When GDPR consent or mobile app access is revoked, or the panel disables the SDK, frames move to the Disabled status and clear their content.

  • App Frames will not be displayed on devices with SDK versions older than the following versions:

    • Android 17.0.0

    • iOS 16.0.0

Integration

1. Enable App Frames

App Frames is off by default. In your app's Info.plist, add a top-level Insider dictionary and put a Boolean App Frames Enabled key inside it, set to YES:

<key>Insider</key>
<dict>
    <key>App Frames Enabled</key>
    <true/>
</dict>

The key must live inside the Insider dictionary. A top-level App Frames Enabled key is ignored. The SDK reads the Insider dictionary first, so a misplaced key leaves App Frames disabled with no warning.

Without this key, every frame rests in the InsiderAppFramesViewStatusDisabled status. The SDK resolves no placements, nothing renders, and no error callback fires. Absent, false, or a non-Boolean value all resolve to disabled. The key is read at startup, so relaunch the app after adding it.

2. Create a placement

A placement is a named slot in your app where inline content can appear. Create it in the InOne under Mobile App Settings > App Frames Placement. Then use its exact ID when you configure the view in code. The placementId string you set on the view must match the placement configured in the panel character-for-character, or no content will be delivered.

3. Add the view

Create the view, assign the placement ID and (optionally) a delegate, and add it to your hierarchy. Give it a width; the height resolves automatically from the rendered content, so do not add a height constraint.

InsiderAppFramesView *framesView = [InsiderAppFramesView new];
framesView.placementId = @"home_page";
framesView.delegate = self;

[self.view addSubview:framesView];

framesView.translatesAutoresizingMaskIntoConstraints = NO;

[NSLayoutConstraint activateConstraints:@[
    [framesView.leadingAnchor
        constraintEqualToAnchor:self.view.leadingAnchor],

    [framesView.trailingAnchor
        constraintEqualToAnchor:self.view.trailingAnchor],

    [framesView.topAnchor
        constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor]

    // No height constraint — the view self-sizes
]];
let framesView = InsiderAppFramesView()
framesView.placementId = "home_page"
framesView.delegate = self

view.addSubview(framesView)

framesView.translatesAutoresizingMaskIntoConstraints = false

NSLayoutConstraint.activate([
    framesView.leadingAnchor.constraint(
        equalTo: view.leadingAnchor
    ),
    framesView.trailingAnchor.constraint(
        equalTo: view.trailingAnchor
    ),
    framesView.topAnchor.constraint(
        equalTo: view.safeAreaLayoutGuide.topAnchor
    )
    // No height constraint — the view self-sizes
])

UIStackView is a natural host for App Frames: stackView.addArrangedSubview(framesView) gives the view its width, the height self-sizes, and when the frame collapses (no content, or dismissed) the stack closes the gap automatically.

InsiderAppFramesView is IB_DESIGNABLE, so you can also add it in a storyboard or XIB:

  1. Drag a UIView into your scene and set its class to InsiderAppFramesView (module InsiderMobile).

  2. Set Placement ID in the Attributes inspector. placementId is IBInspectable.

  3. Wire up the delegate outlet to your view controller and adopt InsiderAppFramesViewDelegate. Delegate is an IBOutlet.

  4. Constrain the leading, trailing, and top edges as usual. Do not pin a height constraint unless you deliberately want a fixed height.

The view subscribes to its placement automatically when it is added to a window and unsubscribes when it leaves one. Setting placementId to nil detaches it from any placement and moves it to the NoPlacement status.

The subscription is tied to the window attachment only. Hiding the view (isHidden = true) does not stop it loading; content still fetches and renders behind the hidden view, so hide or remove it purely as a layout decision.

4. Wire the delegate

Adopt InsiderAppFramesViewDelegate to observe the loading lifecycle and user interactions. Every method is optional, and every callback is delivered on the main actor. A typical integration only needs the dismiss and action callbacks; the rest are informational.

Both appFramesView:didFailLoadingWithError: and appFramesView:didChangeStatusTo:from: fire only on a status transition, and the failure callback always fires first. If the frame is already in an error state and a further failure lands on that same status, the error property is updated but neither callback fires again. Do not use the failure callback to count failures; use it to react to entering a failure state.

The frame's content-lifecycle status changed. This is the single entry point for observing the frame's lifecycle, replacing separate "did start loading" / "did finish loading" callbacks. Refer to Track the frame status.

- (void)appFramesView:(InsiderAppFramesView *)view
    didChangeStatusTo:(InsiderAppFramesViewStatus)status
                 from:(InsiderAppFramesViewStatus)previousStatus;
func appFramesView(_ view: InsiderAppFramesView,
                   didChangeStatusTo status: InsiderAppFramesViewStatus,
                   from previousStatus: InsiderAppFramesViewStatus)

The placement failed to resolve, download, or render its content. The error is always in InsiderAppFramesErrorDomain. Use this callback for logging and diagnostics; the view has already collapsed, so prefer letting it collapse over showing an error. A placement with no campaign does not reach this callback. That is the Unavailable status, not an error.

- (void)appFramesView:(InsiderAppFramesView *)view
didFailLoadingWithError:(NSError *)error;
func appFramesView(
    _ view: InsiderAppFramesView,
    didFailLoading error: Error
)

The template reported the optimal height, in points, for its content. The view already applies this automatically through intrinsicContentSize, so most integrations can ignore this callback and implement it only when they drive the view's height themselves.

- (void)appFramesView:(InsiderAppFramesView *)view
didRequestHeightChangeTo:(CGFloat)optimalHeight;
func appFramesView(
    _ view: InsiderAppFramesView,
    didRequestHeightChangeTo optimalHeight: CGFloat
)

The content was requested to be closed as a result of a user action, such as a close button within the template. The SDK does not hide or remove the view for you. Decide how to react, such as removing the view or collapsing the slot it occupied.

- (void)appFramesViewDidRequestDismiss:(InsiderAppFramesView *)view;
func appFramesViewDidRequestDismiss(_ view: InsiderAppFramesView)

The user interacted with an actionable element inside the rendered content. The dictionary carries the action's custom data from Action Builder, and the callback only fires when that data is non-empty. The SDK has already performed the action's own navigation. Do not repeat it here.

- (void)appFramesView:(InsiderAppFramesView *)view
    didTriggerActionWithData:(NSDictionary<NSString *, id> *)actionData;
func appFramesView(
    _ view: InsiderAppFramesView,
    didTriggerAction actionData: [String: Any]
)

5. Track the frame status

Every frame exposes its current phase through the read-only status property, an InsiderAppFramesViewStatus, and reports every change through appFramesView:didChangeStatusTo:from:. During a successful load, a frame moves forward through ResolvingDownloadingRenderingReady. Ready is the only status in which content is on screen.

Status

Value

Meaning

InsiderAppFramesViewStatusDetached

0

Idle. The view is not attached to a window. A view's initial status.

InsiderAppFramesViewStatusNoPlacement

1

Idle. placementId is nil or empty.

InsiderAppFramesViewStatusResolving

2

The SDK is fetching which campaign targets this placement.

InsiderAppFramesViewStatusDownloading

3

The SDK is downloading the resolved campaign.

InsiderAppFramesViewStatusRendering

4

The content is rendering but is not yet on screen; the frame stays collapsed.

InsiderAppFramesViewStatusUnavailable

5

No campaign targets this placement. Not an error; the frame renders nothing at zero height.

InsiderAppFramesViewStatusReady

6

The content has rendered correctly. The only visible status.

InsiderAppFramesViewStatusDismissed

7

The content closed itself, usually via a close button inside the template.

InsiderAppFramesViewStatusErrorResolving

8

The placements fetch failed, so no campaign could be resolved.

InsiderAppFramesViewStatusErrorDownloading

9

A campaign was resolved, but its content failed to download, or its render URL was untrusted.

InsiderAppFramesViewStatusErrorRendering

10

The content downloaded, but the frame failed to render it.

InsiderAppFramesViewStatusDisabled

11

App Frames is not running. Not an error, see below.

Three helpers keep you from spelling out each case. In Swift, they surface as computed properties on the status:

  • status.isLoading: true for Resolving, Downloading, and Rendering.

  • status.isError: true for the three Error… states, exactly the statuses in which the view's error property is non-nil.

  • status.stringValue: The lowerCamelCase name of the status (e.g., "resolving"), convenient for logging.

None of the resting states are latched (except Disabled in its Info.plist form): the frame moves forward again when fresh content arrives.

func appFramesView(_ view: InsiderAppFramesView,
                   didChangeStatusTo status: InsiderAppFramesViewStatus,
                   from previousStatus: InsiderAppFramesViewStatus) {
    switch status {
    case .ready:              showFrameContainer()
    case _ where status.isLoading: showLoadingPlaceholder()
    default:                  collapseFrameContainer()   // unavailable, dismissed, disabled, error…
    }
}

Disabled is not a failure. It means App Frames is not running at all, and it happens two ways.

Not opted in: The App Frames Enabled Info.plist key is absent or NO, terminal for the process; set it to YES and relaunch.

Consent denied or SDK disabled: GDPR consent revoked, mobile app access revoked, or the panel disabled the SDK. This also cancels the in-flight resolution and drops the resolved selection. Reversible, but restoring consent alone does not bring the frame back immediately: the SDK re-resolves on the following session start, the one moment the identity the request needs is known to be settled.

6. Handle dismissal

When the user closes the frame from inside the template, the SDK moves the status to Dismissed, so appFramesView:didChangeStatusTo:from: fires first; then calls appFramesViewDidRequestDismiss:, and does nothing else to your layout. The view stays in your hierarchy until you act. Remove it or hide it. Inside a UIStackView, either one collapses the slot and closes the gap.

- (void)appFramesViewDidRequestDismiss:(InsiderAppFramesView *)view {
    [view removeFromSuperview]; // In a stack view, the stack closes the gap.
    // or: view.hidden = YES; // Also collapses inside a UIStackView.
}
func appFramesViewDidRequestDismiss(_ view: InsiderAppFramesView) {
    view.removeFromSuperview() // In a stack view, the stack closes the gap.
    // or: view.isHidden = true // Also collapses inside a UIStackView.
}

If your templates include a close button and you do not implement this callback, tapping the button appears to do nothing. The dismissal itself is tracked automatically by the SDK either way.

7. Handle actions

The SDK already performs the action's own navigation. When a template triggers a deep link, the SDK opens it for you before it calls appFramesView:didTriggerActionWithData:. Do not open it again from the callback; you will navigate twice.

This callback exists for the extra data a campaign attaches to an action, not for the navigation itself. The dictionary you receive is the action's custom object as configured in Action Builder, and the callback fires only when that object is present and non-empty. An action with no custom data never reaches your delegate at all.

Use it for app-side side effects the campaign's own action cannot express, e.g., analytics tagging or refreshing a screen. Its keys are those your marketing team configures, so you can agree on a convention.

- (void)appFramesView:(InsiderAppFramesView *)view
    didTriggerActionWithData:(NSDictionary<NSString *, id> *)actionData {

    // The SDK has already handled any deep link on this action.
    NSString *campaignTag = actionData[@"campaign_tag"];

    if (campaignTag.length > 0) {
        NSLog(@"App Frame action: %@", campaignTag);
    }
}
func appFramesView(
    _ view: InsiderAppFramesView,
    didTriggerAction actionData: [String: Any]
) {
    // The SDK has already handled any deep link on this action.
    if let campaignTag = actionData["campaign_tag"] as? String {
        print("App Frame action: \(campaignTag)")
    }
}

Click analytics are recorded automatically; no element is tracked for attribution.

8. Manage height

By default, the view sizes itself to its content through intrinsicContentSize. Give it a width through constraints or a stack view, and never pin a height constraint. When the template's content changes, the view invalidates its intrinsic size, and Auto Layout automatically resolves the new height; when no content is available, the intrinsic height is zero, and the view collapses.

To manage the height yourself, pin a height constraint. The SDK then respects your size verbatim and never fights your constraints. In this fixed-size mode, appFramesView:didRequestHeightChangeTo: tells you the height the template would like to occupy. Use it to drive your own constraint for content-matched sizing under manual control.

- (void)appFramesView:(InsiderAppFramesView *)view
didRequestHeightChangeTo:(CGFloat)optimalHeight {

    self.frameHeightConstraint.constant = optimalHeight;

    [UIView animateWithDuration:0.3
                     animations:^{
        [self.view layoutIfNeeded];
    }];
}
func appFramesView(
    _ view: InsiderAppFramesView,
    didRequestHeightChangeTo optimalHeight: CGFloat
) {
    frameHeightConstraint.constant = optimalHeight

    UIView.animate(withDuration: 0.3) {
        self.view.layoutIfNeeded()
    }
}

Collapse is not reported through this callback. When the frame dismisses, errors out, or loses its content, it zeroes its own intrinsicContentSize without firing appFramesView:didRequestHeightChangeTo:. If you drive the height manually, you must zero your constraint when the status becomes NoPlacement, Unavailable, Disabled, Dismissed, or any Error… state. Observe appFramesView:didChangeStatusTo:from: for this.

Never reserve fixed empty space in case a frame arrives. The fail-silent contract assumes an empty placement occupies no space. A fixed-height placeholder shows the user an empty box whenever no campaign is active.

9. Handle errors

Every failure surfaced by App Frames is an NSError in InsiderAppFramesErrorDomain, with a code from InsiderAppFramesErrorCode. Where an originating error exists, such as a network error, it is available under NSUnderlyingErrorKey. The same error is also readable at any time from the view's error property, which is non-nil exactly while the status is one of the Error… states.

Error code

Value

Status

Meaning

InsiderAppFramesErrorCodeUnknown

0

An unclassified error; the fallback when no more specific code applies.

InsiderAppFramesErrorCodeResolutionFailed

1

ErrorResolving

The request to fetch the set of App Frames placements failed (e.g., due to a network or server error). The originating error is under NSUnderlyingErrorKey.

InsiderAppFramesErrorCodeResponseMalformed

2

ErrorResolving

The placements request succeeded, but its body could not be reconciled (e.g., the placements key is missing), so no placement was routed.

InsiderAppFramesErrorCodeDownloadingFailed

3

ErrorDownloading

The placement's content failed to download. The originating error is under NSUnderlyingErrorKey.

InsiderAppFramesErrorCodePlacementUntrusted

4

ErrorDownloading

The placement's render URL failed trust validation (it is not an absolute http(s) URL with a host), so its content was never fetched.

InsiderAppFramesErrorCodeContentDisplayFailed

5

ErrorRendering

The placement's content was downloaded but could not be displayed. The originating error is under NSUnderlyingErrorKey.

InsiderAppFramesErrorCodeRenderingFailed

6

ErrorRendering

The rendered template reported an error dismissal. The template's raw dismiss code is under the dismiss_code key in userInfo; you do not need it for normal integration.

- (void)appFramesView:(InsiderAppFramesView *)view didFailLoadingWithError:(NSError *)error {
    if (![error.domain isEqualToString:InsiderAppFramesErrorDomain]) return;

    switch (error.code) {
        case InsiderAppFramesErrorCodeResolutionFailed:
            NSLog(@"Placements fetch failed: %@", error.userInfo[NSUnderlyingErrorKey]);
            break;
        case InsiderAppFramesErrorCodeDownloadingFailed:
        case InsiderAppFramesErrorCodePlacementUntrusted:
            NSLog(@"App Frame content unavailable: %@", error.localizedDescription);
            break;
        default:
            NSLog(@"App Frames error: %@", error.localizedDescription);
            break;
    }
}

In Swift, App Frames errors bridge to the InsiderAppFramesError struct with a nested Code enum, so you can cast and switch directly.

func appFramesView(_ view: InsiderAppFramesView, didFailLoading error: Error) {
    guard let frameError = error as? InsiderAppFramesError else { return }

    switch frameError.code {
    case .resolutionFailed:
        let underlying = (error as NSError).userInfo[NSUnderlyingErrorKey]
        print("Placements fetch failed: \(String(describing: underlying))")
    case .downloadingFailed, .placementUntrusted:
        print("App Frame content unavailable: \(error.localizedDescription)")
    default:
        print("App Frames error: \(error.localizedDescription)")
    }
}

"No campaign right now" is not an error. When a placement has no active campaign for this user, the frame moves to the UNAVAILABLE status and appFramesView:didFailLoadingWithError: is never called. Do not look for an error code for this case; observe appFramesView:didChangeStatusTo:from: instead. Likewise, treat every error above as "collapse the slot", not "show an error to the user".

Limitations

  • App Frames must be opted in via the App Frames Enabled key in the Insider dictionary in Info.plist. Placed anywhere else, it is silently ignored, and every frame rests in Disabled with nothing ever requested.

  • A placement can have at most 10 active campaigns at once.

  • Campaign priority is High/Medium/Low; when two campaigns tie, the most recently created one wins.

  • Template images are limited to 500 KB or less per asset.

  • Placements must be defined on the SDK side, embedded in the app and shipped in a release, before marketers can target them. Adding a new placement requires an app release.

  • App Frames requires Insider One’s iOS SDK 16.0.0 or later. On older SDK versions, frames are not displayed.

  • App Frames fail silently: an empty placement renders nothing and collapses to zero height, and the app must not reserve space for it.

  • Pinning a height constraint by accident switches the view to fixed-size mode, clips content, and prevents the view from collapsing when empty. Only pin a height deliberately.

  • Reserving space for the frame in your layout displays an empty box when no campaign is active. Let the frame collapse; stack views handle this for free.

  • Forgetting the dismiss callback leaves an in-template close button appearing to do nothing, because the SDK never hides the view.

  • InsiderAppFramesView cannot be subclassed. The compiler rejects a subclass in both Objective-C and Swift.

    delegate is weak. Make sure the delegate object, usually the view controller, outlives the view, or callbacks silently stop.

  • Unavailable is the normal "no campaign right now" signal and produces no error callback at all.

  • Revoking GDPR consent or mobile app access moves every frame to Disabled and clears content already on screen. Frames return on the next session start after consent is restored, not immediately.

Troubleshooting

Symptom

Likely cause

Fix

Nothing ever happens; status is Disabled from the start.

The App Frames Enabled key is missing, NO, or placed outside the Insider dictionary.

Add the Boolean key inside a top-level Insider dictionary, set to YES, and relaunch the app.

The status is Disabled after it used to work.

GDPR consent or mobile app access was revoked, or the panel disabled the SDK.

Restore consent; frames return on the next session start, not immediately.

The frame never appears; status is Unavailable.

No active campaign targets the placement, or the placement is deactivated in the panel.

Expected behavior: the view collapses. Check Mobile App Settings > App Frames Placement and the campaign's status and segment.

The frame never appears; status stays NoPlacement or Detached.

placementId never set, placementId typo, or the view never attached to the window.

Verify the id matches the panel exactly; confirm the view is in the visible hierarchy.

The status is ErrorResolving, didFailLoading reports ResolutionFailed.

Network or server error while fetching placements.

Usually transient. The SDK retries automatically with backoff. Check connectivity and the underlying error under NSUnderlyingErrorKey.

The status is ErrorDownloading with PlacementUntrusted.

The campaign's render URL is not a valid absolute http(s) URL with a host

Report the campaign to Insider One. This is a server-side content issue, not an integration one.

The frame shows but with the wrong height or clipped content.

A height constraint or fixed frame is pinned on the view.

Remove the height constraint for self-sizing, or drive it from didRequestHeightChangeTo:.

The frame stays at its previous height after the campaign ends.

You drive the height manually and did not zero it on collapse.

Collapse is not reported through didRequestHeightChangeTo:. Zero the constraint in didChangeStatusTo:from: for Unavailable, Dismissed, Disabled, and the Error… states.

The close button inside the frame does nothing.

appFramesViewDidRequestDismiss: is not implemented.

Implement the callback and hide or remove the view there.

Callbacks stop firing after a while.

delegate is weak, and the delegate object was deallocated.

Keep a strong reference to the delegate (usually the view controller) for as long as the view lives.

Content seems stale after a campaign change.

Rendered content is cached in memory for the app process.

Content refreshes on app foreground and identity change; a fresh process always fetches anew.

Full example

A complete view controller embedding two placements in a scrollable stack. When a frame is empty, dismissed, or fails, the stack automatically closes the gap.

#import <UIKit/UIKit.h>
#import <InsiderMobile/Insider.h>

@interface HomeViewController : UIViewController <InsiderAppFramesViewDelegate>

@property (nonatomic, strong) UIStackView *stackView;

@end

@implementation HomeViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.stackView = [UIStackView new];
    self.stackView.axis = UILayoutConstraintAxisVertical;
    self.stackView.spacing = 16;
    // … install stackView in a scroll view, constrain leading/trailing/top/bottom …

    [self addFrameWithPlacementId:@"home_top_banner"];
    // … your own content in between …
    [self addFrameWithPlacementId:@"home_bottom_banner"];
}

- (void)addFrameWithPlacementId:(NSString *)placementId {
    InsiderAppFramesView *framesView = [InsiderAppFramesView new];
    framesView.placementId = placementId;
    framesView.delegate = self;
    [self.stackView addArrangedSubview:framesView];
}

#pragma mark - InsiderAppFramesViewDelegate

- (void)appFramesView:(InsiderAppFramesView *)view
    didChangeStatusTo:(InsiderAppFramesViewStatus)status
                 from:(InsiderAppFramesViewStatus)previousStatus {
    // Ready is the only visible status; everything else collapses silently.
    NSLog(@"%@: %@ -> %@", view.placementId,
          InsiderAppFramesViewStatusStringValue(previousStatus),
          InsiderAppFramesViewStatusStringValue(status));
}

- (void)appFramesView:(InsiderAppFramesView *)view didFailLoadingWithError:(NSError *)error {
    // Fail silently — the view has already collapsed. Log for diagnostics only.
    NSLog(@"Frame failed: %@ — %@", view.placementId, error.localizedDescription);
}

- (void)appFramesViewDidRequestDismiss:(InsiderAppFramesView *)view {
    [view removeFromSuperview];
}

- (void)appFramesView:(InsiderAppFramesView *)view didTriggerActionWithData:(NSDictionary<NSString *, id> *)actionData {
    // The SDK has already handled the action's own navigation; this is the campaign's custom data.
    NSLog(@"Frame action: %@", actionData);
}

@end
import UIKit
import InsiderMobile

final class HomeViewController: UIViewController {

    private let stackView = UIStackView()

    override func viewDidLoad() {
        super.viewDidLoad()

        stackView.axis = .vertical
        stackView.spacing = 16
        // … install stackView in a scroll view, constrain leading/trailing/top/bottom …

        addFrame(placementId: "home_top_banner")
        // … your own content in between …
        addFrame(placementId: "home_bottom_banner")
    }

    private func addFrame(placementId: String) {
        let framesView = InsiderAppFramesView()
        framesView.placementId = placementId
        framesView.delegate = self
        stackView.addArrangedSubview(framesView)
    }
}

extension HomeViewController: InsiderAppFramesViewDelegate {

    func appFramesView(_ view: InsiderAppFramesView,
                       didChangeStatusTo status: InsiderAppFramesViewStatus,
                       from previousStatus: InsiderAppFramesViewStatus) {
        // .ready is the only visible status; everything else collapses silently.
        print("\(view.placementId ?? "-"): \(previousStatus.stringValue) -> \(status.stringValue)")
    }

    func appFramesView(_ view: InsiderAppFramesView, didFailLoading error: Error) {
        // Fail silently — the view has already collapsed. Log for diagnostics only.
        print("Frame failed: \(view.placementId ?? "-") — \(error.localizedDescription)")
    }

    func appFramesViewDidRequestDismiss(_ view: InsiderAppFramesView) {
        view.removeFromSuperview()
    }

    func appFramesView(_ view: InsiderAppFramesView, didTriggerAction actionData: [String: Any]) {
        // The SDK has already handled the action's own navigation; this is the campaign's custom data.
        print("Frame action: \(actionData)")
    }
}