> ## Documentation Index
> Fetch the complete documentation index at: https://docs.trypillow.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Present studies

> Show research studies to users inside your iOS app

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

For the concepts behind this, see [About presenting studies](/sdk/concepts/presenting-studies).

## Basic usage

Present a study by its ID. You can find the ID in the **Integration** tab of your study settings.

```swift theme={"theme":{"light":"github-light","dark":"github-dark"}}
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`:

```swift theme={"theme":{"light":"github-light","dark":"github-dark"}}
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:

```swift theme={"theme":{"light":"github-light","dark":"github-dark"}}
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:

```swift theme={"theme":{"light":"github-light","dark":"github-dark"}}
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.

```swift theme={"theme":{"light":"github-light","dark":"github-dark"}}
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.

<Tip>
  Use the delegate to track study engagement, trigger follow-up actions after an interview, or show a fallback if the study fails to load.
</Tip>

## 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:**

```swift theme={"theme":{"light":"github-light","dark":"github-dark"}}
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.

```swift theme={"theme":{"light":"github-light","dark":"github-dark"}}
// SceneDelegate
func sceneDidBecomeActive(_ scene: UIScene) {
    PillowSDK.shared.onReadyToPresentStudy()
}
```

If your app uses `UIApplicationDelegate` without scenes, use the app lifecycle hook instead:

```swift theme={"theme":{"light":"github-light","dark":"github-dark"}}
// AppDelegate
func applicationDidBecomeActive(_ application: UIApplication) {
    PillowSDK.shared.onReadyToPresentStudy()
}
```

<Warning>
  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.
</Warning>

### Manual check

To force an immediate check (for example, after a specific user action), call `presentLaunchStudyIfAvailable()`:

```swift theme={"theme":{"light":"github-light","dark":"github-dark"}}
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.

<Tip>
  To set up audience targeting and launch distributions, see the [audience targeting guide](/guides/audience-targeting).
</Tip>

## 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

<Warning>
  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.
</Warning>
