Skip to main content
You control when and where to present studies inside your app. For the concepts behind this, see About presenting studies.

Basic usage

Present a study by its ID. You can find the ID in the Integration tab of your study settings.
PillowSDK.shared.present(
    study: PillowStudy(id: "your-study-id-here")
)

Session resumption

By default, the SDK remembers where a user left off. If a user partially completes a study and you call present(study:) again with the same ID, they resume from where they stopped.

Start a fresh session

To force a new interview instead of resuming, pass presentation options with forceFreshSession set to true:
PillowSDK.shared.present(
    study: PillowStudy(id: "your-study-id-here"),
    options: PillowStudyPresentationOptions(
        forceFreshSession: true,
        skipIfAlreadyExposed: false
    )
)
This clears the stored session for that study and starts a new interview from the beginning.

Skip repeat exposure

To ask the backend to no-op when the current SDK user was already exposed to the same study, pass presentation options:
PillowSDK.shared.present(
    study: PillowStudy(id: "your-study-id-here"),
    options: PillowStudyPresentationOptions(
        forceFreshSession: false,
        skipIfAlreadyExposed: true
    )
)
skipIfAlreadyExposed defaults to false, so existing calls keep presenting unless you opt in.

Monitor lifecycle

Pass a delegate to present(study:delegate:) to receive lifecycle callbacks:
class MyStudyDelegate: PillowStudyDelegate {
    func studyDidPresent(_ study: PillowStudy) {
        print("Study appeared on screen")
    }
    func studyDidSkip(_ study: PillowStudy) {
        print("Study was skipped")
    }
    func studyDidFinish(_ study: PillowStudy) {
        print("User finished the study")
    }
    func studyDidFailToLoad(_ study: PillowStudy, error: Error) {
        print("Study failed: \(error.localizedDescription)")
    }
}

let studyDelegate = MyStudyDelegate()

PillowSDK.shared.present(
    study: PillowStudy(id: "your-study-id-here"),
    delegate: studyDelegate
)
Retain your delegate for as long as you need callbacks. The SDK keeps only a weak reference to avoid retain cycles. In SwiftUI, the common pattern is to retain a small coordinator object in @State or @StateObject and pass that object as the delegate.
import SwiftUI
import PillowSDK

struct ContentView: View {
    @State private var studyCoordinator: StudyPresentationCoordinator?

    var body: some View {
        Button("Start feedback") {
            let coordinator = StudyPresentationCoordinator(
                onFinished: { _ in
                    studyCoordinator = nil
                },
                onFailed: { _, error in
                    print(error.localizedDescription)
                    studyCoordinator = nil
                }
            )

            studyCoordinator = coordinator
            PillowSDK.shared.present(
                study: PillowStudy(id: "your-study-id-here"),
                delegate: coordinator
            )
        }
    }
}

private final class StudyPresentationCoordinator: PillowStudyDelegate {
    private let onFinished: (PillowStudy) -> Void
    private let onFailed: (PillowStudy, Error) -> Void

    init(
        onFinished: @escaping (PillowStudy) -> Void = { _ in },
        onFailed: @escaping (PillowStudy, Error) -> Void = { _, _ in }
    ) {
        self.onFinished = onFinished
        self.onFailed = onFailed
    }

    func studyDidFinish(_ study: PillowStudy) {
        Task { @MainActor in
            onFinished(study)
        }
    }

    func studyDidFailToLoad(_ study: PillowStudy, error: Error) {
        Task { @MainActor in
            onFailed(study, error)
        }
    }
}
MethodDescription
studyDidPresent(_:)The study modal appeared on screen
studyDidSkip(_:)The study was intentionally skipped and not presented
studyDidFinish(_:)The user finished or dismissed the study
studyDidFailToLoad(_:error:)The study could not be loaded or presented. Access error.localizedDescription for the cause, such as a network error or another study already being shown
All delegate methods are invoked on the main thread, so you can safely update your UI from them. Implement the methods you need.
Use the delegate to track study engagement, trigger follow-up actions after an interview, or show a fallback if the study fails to load.

Launch studies

Launch studies are presented automatically to targeted SDK users based on audience filters you configure in the dashboard. Instead of calling present(study:) in your code, the backend tells the SDK which study to show.

Enable automatic presentation

Call onReadyToPresentStudy() when your app UI is in a state where it’s safe to present a study. The SDK then automatically presents pending launch studies when the app comes to the foreground. SwiftUI:
struct ReadyToPresentStudyModifier: ViewModifier {
    @Environment(\.scenePhase) private var scenePhase

    func body(content: Content) -> some View {
        if #available(iOS 17.0, *) {
            content.onChange(of: scenePhase) { _, newPhase in
                if newPhase == .active {
                    PillowSDK.shared.onReadyToPresentStudy()
                }
            }
        } else {
            content.onChange(of: scenePhase) { newPhase in
                if newPhase == .active {
                    PillowSDK.shared.onReadyToPresentStudy()
                }
            }
        }
    }
}

// Apply to your root view
WindowGroup {
    ContentView()
        .modifier(ReadyToPresentStudyModifier())
}
UIKit: Call onReadyToPresentStudy() on every scene activation so the SDK is re-armed when the app returns from the background.
// SceneDelegate
func sceneDidBecomeActive(_ scene: UIScene) {
    PillowSDK.shared.onReadyToPresentStudy()
}
If your app uses UIApplicationDelegate without scenes, use the app lifecycle hook instead:
// AppDelegate
func applicationDidBecomeActive(_ application: UIApplication) {
    PillowSDK.shared.onReadyToPresentStudy()
}
Do not call onReadyToPresentStudy() from viewDidAppear(_:). It only runs when a view controller first appears, so pending launch studies won’t present after the app returns from the background.

Manual check

To force an immediate check (for example, after a specific user action), call presentLaunchStudyIfAvailable():
PillowSDK.shared.presentLaunchStudyIfAvailable()
You can pass an optional delegate to receive lifecycle callbacks, just like present(study:delegate:). If no launch study is pending, the call is a no-op.

Presentation

Launch studies are presented as the same native modal as manual present(study:) calls. Any web_display settings configured in the dashboard are forwarded to the hosted web experience only. They do not change the native iOS presentation.
To set up audience targeting and launch distributions, see the audience targeting guide.

Best practices

  • Call setExternalId() before presenting so the study is linked to the right user
  • Copy the study ID from the Integration tab in your study settings
  • Test in test mode first: test interviews don’t appear in your dashboard data
Make sure the study is set to live mode before presenting it to real users. Studies in test mode work for testing but data won’t appear in your production dashboard.