Embedded Content

Integrate Embedded Content into your iOS app to display Scene content directly within your app’s screens.

View as Markdown

For information about Embedded Content, including overview, use cases, and how to create Embedded Content view styles and Scenes, see Embedded Content.

Adding an embedded view

The AirshipEmbeddedView is a SwiftUI view that defines a place for an Airship Embedded Content to be displayed. When defining an AirshipEmbeddedView, specify the embeddedId for the content it should display. The value of the embeddedId must be the ID of an Embedded Content view style in your project.

Basic integration
// Show any "home_banner" Embedded Content
AirshipEmbeddedView(embeddedID: "home_banner")

Placeholders

If content is unavailable to display, the default behavior is to show an EmptyView. You can customize this by providing a placeholder.

Basic integration with placeholder
AirshipEmbeddedView(embeddedID: "home_banner") {
    Text("Placeholder!")
}

Selection

When more than one instance of embedded content is pending for the same embedded ID, AirshipEmbeddedView uses a selection strategy to decide which one to display. Pass a selection argument to the view initializer.

Priority

.priority is the default strategy. Once an instance is displayed, it stays displayed for as long as it remains pending, so the displayed content doesn’t change while the user is viewing it. When no previously displayed instance is pending, the highest-priority one is chosen. A newly arrived instance with a better priority does not replace what’s on screen while that instance is still pending. Target it with .instance if you need it to win immediately.

AirshipEmbeddedView(embeddedID: "home_banner", selection: .priority)

Comparator

.comparator sorts pending instances with a closure you supply. Return .orderedAscending to display lhs before rhs. The first instance in the sorted order is displayed.

AirshipEmbeddedView(embeddedID: "home_banner", selection: .comparator { lhs, rhs in
    let lScore = lhs.extras?["score"]?.int ?? 0
    let rScore = rhs.extras?["score"]?.int ?? 0
    if lScore != rScore { return lScore > rScore ? .orderedAscending : .orderedDescending }
    return .orderedSame
})

Instance

.instance pins the view to a specific pending instance by its instanceID. Instance IDs are runtime-generated UUIDs, so obtain one from an AirshipEmbeddedObserver before passing it. The placeholder is shown until that exact instance is pending.

@StateObject private var observer = AirshipEmbeddedObserver(embeddedID: "home_banner")

var body: some View {
    AirshipEmbeddedView(
        embeddedID: "home_banner",
        selection: observer.embeddedInfos.first.map { .instance($0.instanceID) } ?? .priority
    )
}

AI selection

iOS SDK 21 beta

.ai uses the on-device model to score each pending instance and display the best match. Set up On-Device AI before using it.

AirshipEmbeddedView(embeddedID: "home_banner", selection: .ai(
    prompt: "Show content that matches the user's current interests."
))

The placeholder is shown while the model runs. If the model is unavailable or the score threshold isn’t met, the fallback strategy is used instead.

The evaluation runs once when the pending set first becomes available, and again each time a new instance becomes pending. Context from the context provider is captured at evaluation time. It is not re-fetched if the user’s context changes after selection. To force a re-evaluation, apply a new .id() to the view so SwiftUI recreates it.

Force re-evaluation
AirshipEmbeddedView(embeddedID: "home_banner", selection: .ai(
    prompt: "Show content that matches the user's current interests."
))
.id(contextVersion) // increment to re-run selection

AI selection options

.ai accepts the following parameters:

ParameterTypeDefaultDescription
promptStringn/aRequired. Instruction describing how to choose among the pending instances. The model scores each candidate 1–10 based on this prompt and the user’s context.
strategyAIConfig.Strategy.scoreThenPriorityHow scores and priorities are combined. .scoreThenPriority uses AI score as the primary sort key. .priorityThenScore uses candidate priority first and breaks ties by score.
minScoreThresholdInt?nilIf set, the top-ranked candidate must reach this score or the AI result is discarded and fallback is used instead.
allowDisplayInterruptionsBoolfalseWhen true, the AI result can interrupt and replace the currently displayed instance. When false, the displayed instance keeps showing until dismissed, even if new pending content arrives.
subjectHints[String: String][:]Per-view key-value pairs passed to the context provider as subject.hints, so the provider can tailor the context it returns.
fallbackFallback.priorityThe non-AI selection strategy (.priority, .comparator, or .instance) to use if the model is unavailable or the score threshold isn’t met.

Providing context to the model

Register a context provider for AirshipAI.EmbeddedSelection.usage to supply user-specific information that helps the model make better decisions. The subject carries the embeddedID, the list of pending instances being ranked, and any hints you passed as subjectHints.

Embedded selection context provider
Airship.ai.setContextProvider(for: AirshipAI.EmbeddedSelection.usage) { subject in
    AirshipAI.Context(items: [
        AirshipAI.Context.Item(content: "User interests: sports, travel", priority: -1),
        AirshipAI.Context.Item(content: "Last banner shown: Sports Highlights"),
    ])
}

If no context provider is registered, the model works from the prompt alone.

Placing in a scroll view

When placed directly in a ScrollView, or a child view within the ScrollView that is allowed to grow unbounded in the scrollable direction, you need to pass the maximum size of the embedded view to make percent-based sizing work correctly. The easiest way is to wrap the ScrollView in a GeometryReader and pass the size info to the embedded view.

GeometryReader example
struct ScrollViewExample: View {
    var body: some View {
        GeometryReader { geometryProxy in
            ScrollView(showsIndicators: false) {

                AirshipEmbeddedView(
                    embeddedID: "home_banner",
                    embeddedSize: AirshipEmbeddedSize(
                        parentBounds: geometryProxy.size
                    )
                )

            }
        }
    }
}

When using a GeometryReader, it takes up as much space as allowed. To avoid this and instead measure the current size of the content, you can use the view extension airshipMeasureView.

airshipMeasureView example
struct ScrollViewExample: View {
    @State var state: CGSize?

    var body: some View {
        ScrollView(showsIndicators: false) {

            AirshipEmbeddedView(
                embeddedID: "home_banner",
                embeddedSize: AirshipEmbeddedSize(
                    parentWidth: state?.width,
                    parentHeight: state?.height
                )
            )

        }
        .airshipMeasureView(self.$state)
    }
}

Styling

You can set a custom style on the embedded view, which allows you to modify how the content is displayed or what pending content is displayed.

In this example, the embedded view has a Dismiss Button above it:

Custom style
public struct CustomEmbeddedViewStyle: AirshipEmbeddedViewStyle {
    @ViewBuilder
    public func makeBody(configuration: AirshipEmbeddedViewStyleConfiguration) -> some View {
        if let selected = configuration.selected {
            VStack {
                Button("Dismiss") {
                    selected.onDismiss()
                }
                selected.content
            }
        } else {
            configuration.placeHolder
        }
    }
}

Setting the style
AirshipEmbeddedView(embeddedID: "home_banner")
    .setAirshipEmbeddedStyle(CustomEmbeddedViewStyle())

Observing available embedded content

Embedded Content is not always available, and even after being triggered, it still needs to be prepared before it can be displayed. An AirshipEmbeddedView will automatically update when content is available and transition from the placeholder to the content once content is available. If you need to query the availability of Embedded Content, you can use an AirshipEmbeddedObserver to watch for updates.

An AirshipEmbeddedObserver is an ObservableObject that you can use as a StateObject to automatically refresh the view when new Embedded Content is available. It allows for more dynamic handling of Embedded Content than just content or a placeholder.

Observable example
struct ObservableExample: View {

    @StateObject
    private var embeddedObserver: AirshipEmbeddedObserver = AirshipEmbeddedObserver(embeddedID: "home_banner")

    @State var tabIndex = 0
    var body: some View {
        if (embeddedObserver.embeddedInfos.isEmpty) {
            Text("No banner available")
        } else {
            Text("Banner available")
            AirshipEmbeddedView(embeddedID: "home_banner")
        }
    }
}

The AirshipEmbeddedObserver can be created to watch for one embeddedID, all embedded IDs, or use custom filtering for embedded IDs. The embeddedInfos is the FIFO order of embedded info, including the extras you can set through the Scene composer when creating the content.