Skip to content

Swift Usage

This page explains how to use Context Decision to calibrate your custom model, and how to make decisions based on the predictions.

  • Step 1: Add ContextSDK to your app
  • Step 2: Ship app into production in Calibration Mode
  • Step 3: Once enough data is collected, you can start making decisions based on the SDK's predictions


  • Use a different, unique flowName for each upsell funnel (e.g. upsell_prompt, upsell_onboarding, etc.)
    • If you have the same purchase flow, but from different parts of your app, please use different a different flowName for each.
    • We automatically analyze the overlap between different flows, to decide if there should be one, or more models.
  • The timing is crucial: You need to create the context object right before showing your prompt, and then log the outcome of the user interaction after the user has either accepted or dismissed the prompt.
  • For each context object you create, be sure to log an outcome, even if the user dismisses the prompt.

We recommend using this approach for most apps, unless you have a specific reason to use the advanced methods.

In most cases, ContextSDK will execute your block instantly. Only if your app was recently launched, or resumed from the background, it will take up to a few seconds to get a full context.

ContextManager.optimize("upsell") { context in
  // [Show the upgrade prompt here right after fetching the context]
  // Once you know if the user purchased or dismissed the upsell, log the outcome:
  context.log(.positive) // or .negative

It's critical you trigger your prompt inside the block, which behaves as follows:

  • The callback is instantly executed if your app has been running for at least 3 seconds
  • The callback being executed within 3 seconds if your app was just put into the foreground recently

Use Instant Callbacks if you want to show a prompt right after a user action (e.g. opening a screen)

There are cases where you may prefer to always instantly have your callback executed. For example, when you want to show a prompt right after a certain user-action, to prevent the prompt showing up when the user already left the screen.

ContextManager.optimize("upsell", maxDelay: 0) { context in
  // [Show the upgrade prompt here right after fetching the context]
  // Once you know if the user purchased or dismissed the upsell, log the outcome:
  context.log(.positive) // or .negative

Using maxDelay: 0 means that the context object may not be complete, which may reduce data quality. We recommend not using this approach right after the app start. Use in places where the app usually has already been running for a few seconds.

If you want full control, you can use the instantContext method to get a Context object instantly. This comes with some extra complexity, as you also need to check the .shouldUpsell property yourself to decide if you should show the prompt or not.

let context = ContextManager.instantContext(flowName: "upsell", duration: 3)

if context.shouldUpsell {
    // [Show the upgrade prompt here right after fetching the context]
    // Once you know if the user purchased or dismissed the upsell, log the outcome:
    context.log(.positive) // or .negative
} else {
    context.log(.skipped) // Context Decision recommends not to show an upsell

During calibration phase, .shouldUpsell will always be true, so your app's behavior won't change. Once your custom model is rolled out, the .shouldUpsell property will reflect the prediction of your model.


Some apps may not have an easy way to pass the context object from where you initially generate it, to where you know the outcome of the user interaction. In those cases, you can use the recentContext method to get the most recent context object you created based on the unique flowName.

  • This approach requires you to create the context object first.
  • Only the most recent context for each flowName name will be stored, and older context objects will be discarded.
  • The context object must already be created before you show the prompt.
ContextManager.optimize("upsell") { context in
  // [Prompt the user for an upgrade right after getting the context]
  // Don't log any outcomes here
// Once the user either finished the purchase, or dismissed the upgrade:
let outcome = EventOutcome.positive // or .negative

if let context = ContextManager.recentContext(flowName: "upsell") {

You can provide additional outcome details, for example to indicate which type of product the user has purchased.

ContextManager.optimize("show_all_deals") { context in
  // [Show the user a list of deals]
  if let selected_deal = user.selectedDeal {
      CustomSignalInt(id: "deal_selected", value: selected_deal.number),
      CustomSignalInt(id: "deal_value_in_usd", value: selected_deal.usd_value)
  } else {

The information provided via the appendOutcomeMetadata will not be used to determine if it's currently a good moment, you can use Custom Signals for that.

The information provided is used to help you decide on variants. For example, you may have different types of offers, or different price points, and you want to train ContextSDK to recommend the best one based on the current user context.

Upsell prompts force the user to make a decision to either accept, or dismiss the prompt. However, when showing a banner, the user may just ignore the banner (not interact with it), and continue using the app. In those cases, you may want to log the outcome of the banner at a later point in time.

When logging an outcome, we recommend the following conventions:

  • .positive: Generic positive events used when the user has to decide on the spot (e.g. full-screen prompt)
    • .positiveInteracted: The user has tapped on the banner, and started the purchase flow, or read more about the offer
    • .positiveConverted: The user ended up successfully purchasing the product (all the way through the payment flow)
  • .negative: Generic negative events used when the user has to decide on the spot (e.g. full-screen prompt)
    • .negativeNotInteracted: The user has not interacted with the banner in any way, and continued using the app
    • .negativeDismissed: The user has actively dismissed the banner (e.g. using a close button)

For the .negativeNotInteracted cases, we recommend logging the outcome when your app is being closed, or if the screen where you show the banner is being closed. For that, you can use the context.logIfNotLoggedYet method:

// You can use your existing code you execute when the user closes your app, 
// or alternatively when the user closes the screen where the banner is shown
func applicationDidEnterBackground(_ application: UIApplication) {
  if let context = ContextManager.recentContext(flowName: "onboarding_upsell") {

Note that as soon as you call .log or .logIfNotLoggedYet on a context, it will be marked as "logged", even if you fetch it again using recentContext.

  • Using .log will always log the outcome, even if it has already been logged before
  • Using .logIfNotLoggedYet will only log the outcome if it hasn't been logged before

When using the block-based syntax using .optimize, there may be situations where you want to cancel the callback due to changed circumstances. For example, the user has navigated to a different screen, and the upsell prompt is no longer relevant.

To do so, you can use the cancelContextCallback method for a specific flowName:

ContextManager.cancelContextCallback(flowName: "upsell")

Custom Signals

ContextSDK uses more than 170 signals based on various built-in sensors to make its predictions. However, you can improve its performance by providing additional data to the ContextSDK that's specific to your app. Some examples of what you could provide: In-game progress, number of friends/messages, number of entries in a list, etc.

Global signals will be used across all ContextSDK calls, and are an easy way to provide additional data to the ContextSDK.

ContextManager.setGlobalCustomSignal(id: "number_of_levels_completed", value: 21)

You can easily override the global custom signals by providing the same id when calling the setGlobalCustomSignal method again using a different value. To remove a global custom signal, call the setGlobalCustomSignal method with a nil value.

In general, we recommend using global signals. But if have custom signals that are specific to a certain flow, you can provide them as a parameter when getting your context object:

let customSignals: [CustomSignal] = [
  CustomSignalInt(id: "number_of_friends", value: 4),

ContextManager.optimize("onboarding_upsell", customSignals: customSignals) { context in
  // ...

Please be sure to consider the user's privacy and don't include any sensitive data. You are responsible for ensuring that you have the necessary rights to provide the data to us. Please at no point include any PII (including but not limited to emails, user IDs, IP addresses, exact locations, etc.) in your custom signals.

Custom Model Rollout

Once you've shipped your app with ContextSDK to the App Store, and you've collected around 1,000 sales events, we can start training your custom machine learning model to optimize your app.

Our recommended approach is to distribute your machine learning models over-the-air directly to your app. This works instantly, uses minimal system resources in the background, and allows us to iterate faster and more efficiently to further improve your app's performance.

In the future, you'll be able to launch your first custom model on our Dashboard, but for the time being, we will reach out to you via email once your first model is ready.

If you're using ContextManager.optimize, no code changes are required.

  • If ContextSDK believes it's currently a good moment to show the prompt, it will run your block
  • If ContextSDK believes it's currently a bad moment to show the prompt, it will not run your block

If you're using the advanced usage using ContextManager.instantContext, you'll need to check the .shouldUpsell property yourself to decide if you should show the prompt or not.

Our OTA rollouts are safe, reliable, and won't affect your app's performance. Our systems continuously monitor the rollout and the resulting conversion performance to ensure everything is working as expected.

If you prefer to disable Over-the-Air updates, we can provide you with a custom SDK binary that includes your custom model. Please drop us a short email at containing your installation method (CocoaPods, SPM, or manual), and we will get you started.

Advanced Usage

Depending on your preference, ContextSDK has a built-in AB test feature. However, if you prefer to use your own AB test system to better compare the results, you can provide the AB test info to ContextSDK.

Step 1: Choose Control Mode

ContextSDK exposes a ContextManager.setControlMode function which allows you to enable control mode. When control mode is enabled, ContextSDK will not make any decisions

ContextManager.setControlMode(enabled: true)

Step 2: Provide AB Test Info

ContextManager.setGlobalCustomSignal(id: "ab_test_name", value: "context_sdk")
ContextManager.setGlobalCustomSignal(id: "ab_test_cohort", value: "cohort_1")

ContextSDK exposes a ContextManager.currentAppliedCustomModelVersion function. This gives you the current version of all custom models currently used in the ContextSDK installation. During calibration phase, the value will always be calibration.

Once your first custom model is ready this will return a string uniquely identifying it. You can track this in your own analytics to observe model rollouts.

The minimum deployment target for ContextSDK is iOS 14.0.

We understand that using a hosted service comes with privacy and security concerns and additional paperwork. We also support the most commonly used analytics systems, such as Firebase, Amplitude, Mixpanel, etc. instead of our own server. This allows you to not send any additional network requests from your app, as well as use your existing infrastructure to use ContextSDK.

Using your own backend however comes with a few downsides:

  • The initial setup will take significantly more time
  • You'll have to periodically share the collected ContextSDK signal data with us, either through some kind of scheduled query or cron-job, or by sending us the data manually
  • If your app has a high volume of active users, you'll have to make sure your existing tools can handle the load, in particular being able to export large amounts of data
  • You won't have access to some of the additional features we provide, such as a real-time dashboard of the ContextSDK performance monitoring, as well as Over-The-Air updates of custom models, and the ability to tweak the prompt intensity level in real-time for each flow
  • There is a significantly higher likelihood of errors, due to having more moving parts (your logging system, your data pipeline, your export scripts)

If you're interested in using your own backend, please reach out to us at to get you started.

ContextSDK runs silently in the background, using almost no device resources. We built it from the ground up to have no effect on the app using it. Unless you have specific concerns about specific configurations, we recommend leaving the default options.

You can override the default configuration by passing a Configuration object to the setup method:

// Below you can see the default values for each configuration option
let configuration = ContextSDK.Configuration(
  // Count the number of touches: Uses a `UITapGestureRecognizer`, that will always immediately `return false` 
  // to not have any impact on your app. We don't send any details on those interactions, besides the total count
  enableTouchBehaviourMonitoring: true,

  // Logs the number of seconds of the current session
  enableSessionDurationMonitoring: true,

  // Counts the number of screenshots taken during the current session
  enableScreenshotMonitoring: true,

  // Send a single ContextSDK event when your app is launched, and when it's put into the background. This allows us 
  // to provide you with more in-depth insights on what contexts have a positive effect on session durations and upsells
  enableAutomaticAppStartAndEndTracking: true,

  // When true, ContextSDK will randomize when the ATT prompt is shown during the calibration phase. 
  // This allows us to determine when the best time to show the prompt is and not always show it immediately.
  allowATTPromptRandomization: false,

  // When true, ContextSDK will report unexpected errors back to us. Those are mostly to ensure smooth rollouts of new Machine Learing Models over the air. 
  // Since CoreML models may introduce issues while parsing them, we'd then report those errors back to us, while also falling back to the default model to guarantee smooth operations
  enableErrorReporting: true

ContextManager.setup("YOUR_LICENSE_KEY", configuration: configuration)

ContextSDK can help you increase your ATT (App Tracking Transparency) opt-in rate, a prompt that you can only trigger once. Asking the user at the right time has a major impact on the opt-in rate, which often directly correlates with your app's revenue.

Before ContextSDK:

ATTrackingManager.requestTrackingAuthorization { authorizationStatus in
  if authorizationStatus == .authorized { /* ... */ }

With ContextSDK:

ContextManager.requestATTrackingAuthorizationForCalibration { attStatus in
  if attStatus.authorizationStatus == .authorized { /* ... */ }

If you use a pre-prompt (a dialog to ask the user before you trigger the system ATT prompt), you can use the context.logAttAuthStatus method:

ContextManager.optimize(flowName: "att") { context in
    PrePromptManager.shared.showPrePromptIfNeeded { prePromptResult in
        if prePromptResult == .shouldShow {
            ATTrackingManager.requestTrackingAuthorization { authStatus in
                context.logAttAuthStatus(authStatus: authStatus)
                if authStatus == .authorized { /* ... */ }
        } else {

Since using ContextSDK for ATT together with a pre-prompt comes with some extra complexity, we recommend reaching out to us, so we can do a screen sharing session together and help leverage ContextSDK to achieve the best ATT opt-in rate possible.

Phase 1: Calibration Phase

During the initial setup (calibration phase requestATTrackingAuthorizationForCalibration), ContextSDK will always trigger the ATT prompt, so your app's behaviour won't change.

Phase 2: Custom Model:

Once we've calibrated the 170 ContextSDK signals for your app, we will provide you with a custom model for ATT. You then replace the following call:

From Calibration Mode:

ContextManager.requestATTrackingAuthorizationForCalibration { attStatus in /* ... */ }
To using ContextSDK to only trigger the ATT prompt during good moments:
ContextManager.requestATTrackingAuthorizationIfGoodMoment { attStatus in /* ... */ }

  • From that moment on, ContextSDK will only trigger the ATT prompt during good moments and may choose to not show the prompt during bad moments.
  • You can use attStatus.didTriggerATTPrompt to check if ContextSDK has triggered the ATT prompt during this call. This does not indicate if the system prompt was shown or not.
  • Using requestATTrackingAuthorizationIfGoodMoment will always execute your callback, no matter if the ATT prompt got triggered.

Note on timing: If you use the above mentioned methods right when the app was launched or resumed, ContextSDK may wait for up to 2 seconds before showing the prompt. If your app is already running, the prompt will be shown immediately.

Choose if ContextSDK can AB test different timings

By default, ContextSDK will keep triggering the ATT prompt immediately when calling requestATTrackingAuthorizationForCalibration. However, for the best results, we recommend allowing ContextSDK to run automatic AB tests to experiment with showing the ATT at a later app start.

If you enable this option, ContextSDK will keep showing the ATT prompt during the first app launch 75% of the times, and will wait up to 10 app launches to trigger the prompt.

To enable this feature, update your ContextManager.setup call to:

let configuration = ContextSDK.Configuration(
  // When true, ContextSDK will randomize when the ATT prompt is shown during the calibration phase.
  // This allows us to determine when the best time to show the prompt is and not always show it immediately.
  allowATTPromptRandomization: true

ContextManager.setup("YOUR_LICENSE_KEY", configuration: configuration)

Done with the integration? Header over to the Release Section to go live with your app!