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)
}
}
}
| Method | Description |
|---|
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.
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.