# Pickers, sheets and system alerts

A flow can drive the photo picker, the document picker, the share sheet and permission prompts that iOS shows over your app. Select their controls by the text they show, like any other control.

These run on the iOS simulator. iOS draws them in a separate process, on top of your app, and Squiggle reads them from the simulator’s accessibility tree. Your flow doesn’t need a mock or a test-only code path to get past an attachment step.

## What a flow can drive

| UI | Examples |
| --- | --- |
| `Sheets and pickers shown over your app` | The photo picker (one photo or several), the document picker and the share sheet |
| `System alerts` | Permission prompts, such as photo library access |

While one of these is up, your app’s own screen is covered. Squiggle stops listing the controls beneath it and refuses to tap them, because a real finger would land on the sheet instead. Agents see the same thing: `describe` marks each control on the sheet `external`.

## Attach photos from the picker

Open the picker, wait for it, tap photos and confirm. The wait names the picker’s `Collections` tab, a label nothing in the app contains (see [Wait for it to open](https://squiggle.sh/docs/flows/system-ui#wait-for-it-to-open)). Every cell in the photo picker is labelled `Photo`, so `nth` picks one by position:

```yaml
- tap: { testID: attach-photos }
- waitFor: { visible: { text: Collections } }
- tap: { text: Photo, nth: 2 }
- tap: { text: Photo, nth: 4 }
- assert: { visible: { text: Show Selected (2) } }
- tap: { text: Add }
- assert: { visible: { testID: attachment-count, text: Attachments 2 } }
```

`nth` counts from 1 in reading order: along the top row from left to right, then the next row. A bare `text: Photo` taps the first cell. For a single-photo picker, the tap on a photo closes the picker and returns it to your app.

The picker shows the simulator’s photo library. A new simulator has a few sample photos. To attach a particular image, add it before the run:

```shell
xcrun simctl addmedia booted ./fixtures/receipt.jpg
```

## Attach a file from the document picker

The document picker opens on `Recents`, or in the folder it was last closed in. Tap the `Browse` tab to start from the list of locations, open one, and tap the file by its name:

```yaml
- tap: { testID: attach-file }
- waitFor: { visible: { text: Browse } }
- tap: { text: Browse }
- tap: { text: On My iPhone }
- waitFor: { visible: { text: invoice-1042 } }
- tap: { text: invoice-1042 }
- assert: { visible: { text: invoice-1042.txt } }
```

A file’s label is its name without the extension, followed by its kind, date and size (`invoice-1042, Text file, 12:00, 30 bytes`), so `text: invoice-1042` matches it. Your app then sees the whole file name.

`On My iPhone` starts empty on a simulator. It is a folder on your Mac, so copy the files your flow needs into it before the run:

```shell
DIR=$(xcrun simctl get_app_container booted com.apple.DocumentsApp groups |
  awk -F'\t' '/FileProvider.LocalStorage/ {print $2}')
cp ./fixtures/invoice-1042.txt "$DIR/File Provider Storage/"
```

## Wait for it to open

A sheet appears a moment after the tap that opens it. Wait for one of its controls with `waitFor: { visible: … }`, which checks again until the control shows or the timeout passes (8 seconds unless you set `timeout`). Use `notVisible` to wait for something to go away.

Pick a label that no text on your own screen contains. Until the sheet covers your screen, `text` can also match part of a label in your app: `text: Photo` matches an `Attach photo` button and `text: Cancel` matches `Upload cancelled`, so the wait passes before the sheet opens.

A tap _on_ the sheet, such as `Add` or `Close`, waits for the screen to stop moving before it touches the sheet, because iOS ignores a touch on a sheet or alert that is still sliding in. It waits again afterwards, so the next step can check your app straight away.

Closing the share sheet:

```yaml
- tap: { testID: share-button }
- waitFor: { visible: { text: Copy } }
- tap: { text: Close }
```

## Answer a permission prompt

Tap the button by its text. Your app is inactive while the prompt is up, so a step that names one of its controls fails until the prompt is answered.

```yaml
- tap: { testID: ask-photos }
- waitFor: { visible: { text: Allow Full Access } }
- tap: { text: Allow Full Access }
```

iOS asks for each permission once. To see the prompt again on the next run, reset the permission first:

```shell
xcrun simctl privacy booted reset photos com.example.app
```

## Limits

- These controls have no `testID`. Select them by their text (`text:`), which is in the simulator’s language. Run flows in one language, or keep one flow per language.
- Text is the label iOS gives the control, which can differ from what you see. The photo picker’s cells read `Photo`, not the file name. To see the labels, open the sheet and ask your agent to `describe` the screen.
- A flow can’t continue in another app your app opens, such as Settings or Safari: iOS suspends your app while it is in the background.
- On a macOS app, only the app’s own windows can be driven.

## When a step fails

> **could not resolve … it is under UI another process draws**
>
> The control is in your app, but a sheet, a picker or a system alert covers it. Add the step that closes or answers it first. If you didn’t expect anything to be open, check the failure screenshot: a permission prompt from an earlier step is the usual cause.

> **The sheet’s control isn’t found right after opening it**
>
> The step ran before the sheet finished opening. Put a `waitFor: { visible: … }` for one of its controls before it.

> **The wait passes, then the next step on the sheet fails**
>
> The wait matched a control in your app rather than on the sheet, because its label contains the text you waited for. Wait on a label only the sheet shows. See [Wait for it to open](https://squiggle.sh/docs/flows/system-ui#wait-for-it-to-open).

> **timed out after 8000ms waiting for visible …**
>
> The sheet never showed that control. Check the failure screenshot: the sheet may not have opened, or its label differs from what you see. Ask your agent to `describe` the screen while it is open to see the labels.
