Unwrap Surveys Mobile SDK

Last updated: March 2, 2026

Version 1.0 | iOS 15+ | Android API 26+

The Unwrap Surveys SDK lets you embed conversational survey flows directly into your iOS and Android apps. Present surveys at the right moment, customize the look and feel, and let Unwrap's AI generate intelligent follow-up questions in real time.


Overview

The SDK provides three core capabilities:

  1. Survey Rendering -- displays survey flows configured in the Unwrap dashboard

  2. Manual Presentation -- launch surveys programmatically at the right moment in your app

  3. Theming & Customization -- match the survey UI to your app's design system

Survey content (questions, screen order, scoring type, AI follow-up behavior) is configured entirely in the Unwrap dashboard. The SDK pulls that config at runtime, so you can update what gets asked without shipping a new app version. The SDK is responsible for rendering, styling, presenting, and collecting responses.

How It Works

  1. Create and configure a survey in the Unwrap dashboard -- choose question types, set prompts, configure AI follow-ups, and define the screen flow.

  2. The dashboard gives you a survey UUID (e.g. srv_onboarding_nps).

  3. Initialize the SDK with that UUID. The SDK downloads the survey configuration at app launch.

  4. Call Unwrap.presentSurvey() wherever you want the survey to appear, and optionally attach metadata at config time or submission time.


Installation

iOS (Swift Package Manager)

Note: The Unwrap Surveys SDK is distributed as a private package. Your team will receive repository access as part of onboarding. If you need access, contact your Unwrap account manager or email sdk-access@unwrap.ai.

Add the Unwrap Surveys package to your project:

https://github.com/unwrap-nlp/unwrap-surveys-ios.git

Or add it to your Package.swift:

dependencies: [
    .package(url: "https://github.com/unwrap-nlp/unwrap-surveys-ios.git", from: "1.0.0")
]

CocoaPods

First, register the private Unwrap spec repo:

pod repo add unwrap https://github.com/unwrap-nlp/specs.git

Then add the pod to your Podfile:

source 'https://github.com/unwrap-nlp/specs.git'
source 'https://cdn.cocoapods.org/'  # Keep the default CDN source for other pods

pod 'UnwrapSurveys', '~> 1.0'

Android (Gradle)

Add the Unwrap maven repository and dependency to your app-level build.gradle. Authentication is required -- your credentials are provided during onboarding.

First, add your Unwrap maven credentials to your project's gradle.properties file (or ~/.gradle/gradle.properties for global config). Do not commit these to source control.

# ~/.gradle/gradle.properties
unwrapMavenUser=your_username_here
unwrapMavenToken=your_token_here

Then add the repository and dependency in your app-level build.gradle:

repositories {
    maven {
        url 'https://maven.unwrap.ai/releases'
        credentials {
            username = project.findProperty("unwrapMavenUser") ?: ""
            password = project.findProperty("unwrapMavenToken") ?: ""
        }
    }
}

dependencies {
    implementation 'ai.unwrap:surveys-sdk:1.0.0'
}

ProGuard/R8: The SDK bundles its own consumer ProGuard rules in the AAR, so no additional keep rules are needed for release builds.


Initialization

Initialize the SDK at app launch with your survey UUID. The SDK downloads the survey configuration from Unwrap and caches it locally. You can optionally pass metadata as free-form key-value pairs that will be attached to all responses from this session.

iOS

Use a @UIApplicationDelegateAdaptor to ensure the SDK is configured before any views render:

import SwiftUI
import UnwrapSurveys

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
    ) -> Bool {
        Unwrap.configure(
            surveyId: "srv_onboarding_nps"
        )
        return true
    }
}

@main
struct MyApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

With optional metadata:

Unwrap.configure(
    surveyId: "srv_onboarding_nps",
    metadata: [
        "userId": "user_8291",
        "plan": "enterprise",
        "company": "Acme Corp",
        "appVersion": "3.2.1"
    ]
)

Android

import ai.unwrap.surveys.Unwrap

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        Unwrap.configure(
            context = this,
            surveyId = "srv_onboarding_nps"
        )
    }
}

With optional metadata:

Unwrap.configure(
    context = this,
    surveyId = "srv_onboarding_nps",
    metadata = mapOf(
        "userId" to "user_8291",
        "plan" to "enterprise",
        "company" to "Acme Corp",
        "appVersion" to "3.2.1"
    )
)

Metadata passed at config time is attached to every survey response in this session. Use it to associate responses with users, accounts, app versions, or any context that helps you analyze feedback in the Unwrap dashboard.


Survey Flows

Survey flows are configured entirely in the Unwrap dashboard. A typical flow looks like this:

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│  Numeric Rating  │────▶│  Open-Ended      │────▶│  AI Follow-Up   │
│  (Score Screen)  │     │  (Why Screen)    │     │  (Detail Screen) │
└─────────────────┘     └─────────────────┘     └─────────────────┘

In the dashboard, you configure the question types, prompts, scoring scales, AI follow-up behavior, and screen order. The dashboard generates a survey UUID that you pass to Unwrap.configure().

The SDK's role is to fetch that configuration at runtime, render the screens, collect responses, and send them back to Unwrap. You control how the survey looks (theming), how it's presented (modal, bottom sheet, inline), and when it appears in your app.


Presenting Surveys

Call Unwrap.presentSurvey() to show the survey. You control where in your app this happens -- after a purchase, at the end of onboarding, when a support ticket is resolved, or anywhere else that makes sense.

You can optionally pass submission-time metadata that will be attached to this specific response in addition to any config-time metadata.

iOS

// Present the survey
Unwrap.presentSurvey { result in
    switch result {
    case .completed(let response):
        print("Score: \(response.score ?? -1)")
        print("Feedback: \(response.openEndedText ?? "")")
    case .dismissed:
        print("User dismissed the survey")
    case .error(let error):
        print("Survey error: \(error)")
    }
}

// Present with submission-time metadata
Unwrap.presentSurvey(
    metadata: [
        "screen": "order_confirmation",
        "orderId": "ord_9381"
    ]
) { result in
    // handle result
}

// Present from a specific view controller (UIKit only).
// Use this when you need to control which view controller presents the survey,
// e.g. in a UIKit-based app or a mixed UIKit/SwiftUI codebase.
Unwrap.presentSurvey(from: viewController)

Android

// Present the survey
Unwrap.presentSurvey { result ->
    when (result) {
        is SurveyResult.Completed -> {
            Log.d("Unwrap", "Score: ${result.response.score}")
            Log.d("Unwrap", "Feedback: ${result.response.openEndedText}")
        }
        is SurveyResult.Dismissed -> {
            Log.d("Unwrap", "User dismissed")
        }
        is SurveyResult.Error -> {
            Log.e("Unwrap", "Error: ${result.error}")
        }
    }
}

// Present with submission-time metadata
Unwrap.presentSurvey(
    metadata = mapOf(
        "screen" to "order_confirmation",
        "orderId" to "ord_9381"
    )
) { result ->
    // handle result
}

// Present from a specific Activity or Fragment
Unwrap.presentSurvey(activity = this)

Metadata Merging

When you pass metadata at both config time and submission time, submission-time values take precedence for duplicate keys.

// Config time
Unwrap.configure(
    surveyId: "srv_post_purchase",
    metadata: ["userId": "user_8291", "plan": "enterprise"]
)

// Submission time
Unwrap.presentSurvey(metadata: ["plan": "pro", "orderId": "ord_9381"]) { result in
    // Final metadata on this response:
    // "userId": "user_8291"  (from config)
    // "plan": "pro"          (submission overrides config)
    // "orderId": "ord_9381"  (from submission)
}

Theming

Customize the survey UI to match your app's design system. Theming controls how dashboard-configured surveys are rendered -- colors, fonts, corner radii, presentation style, and animations.

iOS

let theme = UnwrapTheme(
    primaryColor: UIColor(red: 108/255, green: 92/255, blue: 231/255, alpha: 1.0),
    backgroundColor: .systemBackground,
    textColor: .label,
    cornerRadius: 16,
    font: .systemFont(ofSize: 16, weight: .medium),
    titleFont: .systemFont(ofSize: 20, weight: .bold),
    buttonStyle: .filled,
    presentationStyle: .bottomSheet,   // .bottomSheet, .fullScreen, .card, .inline
    overlayOpacity: 0.4,
    animation: .spring
)

Unwrap.setTheme(theme)

Android

val theme = UnwrapTheme(
    primaryColor = Color.parseColor("#6C5CE7"),
    backgroundColor = Color.WHITE,
    textColor = Color.BLACK,
    cornerRadius = 16.dp,
    fontFamily = ResourcesCompat.getFont(context, R.font.inter),
    titleFontSize = 20.sp,
    bodyFontSize = 16.sp,
    buttonStyle = ButtonStyle.FILLED,
    presentationStyle = PresentationStyle.BOTTOM_SHEET,
    overlayOpacity = 0.4f,
    animation = AnimationType.SPRING
)

Unwrap.setTheme(theme)

Presentation Styles

StyleDescription

.bottomSheet

Slides up from the bottom. Default.

.fullScreen

Full-screen modal with navigation bar.

.card

Centered card with backdrop overlay.

.inline

Embeds directly in your view hierarchy (no modal). Used automatically by UnwrapSurveyView in SwiftUI and Jetpack Compose. See SwiftUI & Compose Support.


Delegate / Listener Callbacks

Listen to survey lifecycle events for analytics or custom behavior.

iOS

class SurveyHandler: UnwrapSurveyDelegate {

    func surveyWillPresent(surveyId: String) {
        // Pause background tasks, analytics, etc.
    }

    func surveyDidPresent(surveyId: String) {
        analytics.track("survey_shown", properties: ["surveyId": surveyId])
    }

    func surveyStepCompleted(surveyId: String, step: SurveyStep, data: StepData) {
        if case .score(let value) = data {
            print("User scored: \(value)")
        }
    }

    func surveyCompleted(surveyId: String, response: SurveyResponse) {
        print("Survey \(surveyId) completed at \(response.completedAt)")
        if let score = response.score {
            print("Score: \(score)")
        }
    }

    func surveyDismissed(surveyId: String, atStep: SurveyStep) {
        analytics.track("survey_dismissed", properties: [
            "surveyId": surveyId,
            "dismissedAt": atStep.name
        ])
    }
}

Unwrap.setDelegate(SurveyHandler())

Android

Unwrap.setSurveyListener(object : UnwrapSurveyListener {

    override fun onSurveyWillPresent(surveyId: String) {
        // Pause background tasks
    }

    override fun onSurveyPresented(surveyId: String) {
        analytics.track("survey_shown", mapOf("surveyId" to surveyId))
    }

    override fun onStepCompleted(surveyId: String, step: SurveyStep, data: StepData) {
        when (data) {
            is StepData.Score -> Log.d("Unwrap", "Score: ${data.value}")
            is StepData.Text -> Log.d("Unwrap", "Text: ${data.value}")
            is StepData.AIFollowUp -> Log.d("Unwrap", "Follow-up: ${data.value}")
        }
    }

    override fun onSurveyCompleted(surveyId: String, response: SurveyResponse) {
        Log.d("Unwrap", "Survey $surveyId completed at ${response.completedAt}")
    }

    override fun onSurveyDismissed(surveyId: String, atStep: SurveyStep) {
        analytics.track("survey_dismissed", mapOf(
            "surveyId" to surveyId,
            "dismissedAt" to atStep.name
        ))
    }
})

Offline Support

The SDK caches the survey configuration locally and queues responses when the device is offline. Responses are synced automatically when connectivity is restored. No additional setup is required.

Queued responses are persisted to disk and survive app restarts. The SDK retries with exponential backoff when connectivity returns. Queued responses expire after 7 days to avoid submitting stale data.

// Check pending response count (iOS)
let pending = Unwrap.pendingResponseCount
print("\(pending) responses waiting to sync")
// Check pending response count (Android)
val pending = Unwrap.pendingResponseCount
Log.d("Unwrap", "$pending responses waiting to sync")

Jetpack Compose & SwiftUI Support

The UnwrapSurveyView component renders a survey inline within your view hierarchy, without presenting a modal. It automatically uses the .inline presentation style regardless of your theme's presentationStyle setting.

SwiftUI

import SwiftUI
import UnwrapSurveys

struct FeedbackView: View {
    var body: some View {
        VStack {
            // The survey renders all content (questions, prompts, follow-ups)
            // as configured in the Unwrap dashboard
            UnwrapSurveyView()
                .frame(height: 300)
        }
    }
}

Jetpack Compose

import ai.unwrap.surveys.compose.UnwrapSurveyView

@Composable
fun FeedbackScreen() {
    Column {
        // The survey renders all content (questions, prompts, follow-ups)
        // as configured in the Unwrap dashboard
        UnwrapSurveyView(
            modifier = Modifier
                .fillMaxWidth()
                .height(300.dp)
        )
    }
}

Testing

Use the preview API to test surveys without affecting production data. Preview mode shows the survey immediately.

// iOS -- Force show the survey
Unwrap.preview()
// Android -- Force show the survey
Unwrap.preview()

Debug Console

Enable verbose logging to see config downloads and API calls:

Unwrap.enableDebugLogging(true)

Sample output:

[Unwrap] Config downloaded for srv_post_purchase
[Unwrap] Presenting survey: srv_post_purchase
[Unwrap] Step completed: score (value: 8)
[Unwrap] Step completed: why (value: "Love the new export feature")
[Unwrap] AI follow-up generated
[Unwrap] Step completed: ai_follow_up_1 (value: "Mostly use it for weekly reports")
[Unwrap] Survey completed -- submitting response
[Unwrap] Response submitted successfully

Data & Privacy

All survey responses are transmitted over TLS 1.3 and stored encrypted at rest. The SDK collects only the data you explicitly pass via metadata. No device fingerprinting, no background data collection.

Note on offline queueing: When the device is offline, responses are queued locally on-device and synced when connectivity returns. This queueing happens only in response to explicit user actions (completing a survey). No data is collected or transmitted in the background outside of this sync.

For GDPR and CCPA compliance, you can disable the SDK for users who opt out of data collection:

Unwrap.setDataCollectionEnabled(false)  // Disables all surveys
Unwrap.setDataCollectionEnabled(false)

Full Integration Example

import SwiftUI
import UnwrapSurveys

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
    ) -> Bool {
        // 1. Initialize with survey UUID and session metadata
        Unwrap.configure(
            surveyId: "srv_post_purchase",
            metadata: [
                "userId": "user_8291",
                "plan": "enterprise",
                "company": "Acme Corp"
            ]
        )

        // 2. Theme (controls how the survey renders)
        Unwrap.setTheme(UnwrapTheme(
            primaryColor: UIColor(red: 108/255, green: 92/255, blue: 231/255, alpha: 1.0),
            presentationStyle: .bottomSheet
        ))

        return true
    }
}

@main
struct MyApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

struct ContentView: View {
    var body: some View {
        Button("Complete Order") {
            completeOrder()

            // 3. Present the survey with submission-time metadata
            Unwrap.presentSurvey(metadata: [
                "orderId": "ord_9381",
                "orderValue": 84.50
            ]) { result in
                if case .completed(let response) = result {
                    print("Score: \(response.score ?? -1)")
                }
            }
        }
    }
}

SDK Versioning

Survey configurations created in the Unwrap dashboard include a schema version. The SDK validates configs against its supported schema version at download time. If the dashboard produces a config for a newer schema than your installed SDK supports, the SDK will fall back to the last compatible cached config and log a warning in debug mode. Keep the SDK updated to access new screen types and configuration options.


Support