Skip to main content
You control when and where to present studies inside your app.

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 conversation 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 conversation 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 — for example, a network error or another study is 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 a conversation, or show a fallback if the study fails to load.

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 conversations 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.