WebDriverAgent turns an iOS picker wheel with one tap, and on the Clock timer that tap moves two rows
A wheel is moved by tapping beside the selected row, not by dragging it. The default offset is a guess about row height, and when the guess is wrong the arithmetic makes the target unreachable while every call reports success.
WebDriverAgent turns an iOS picker wheel with a single tap placed a fraction of the wheel's
height from its centre, not with a drag. The default fraction is 0.2, and on the iOS Clock timer
wheel, which is 292 points tall, that tap lands two rows away, so the wheel advances two rows per
tap. Asking for 20 minutes from a wheel sitting on 5 therefore walked 40 taps from 5 to 35 and
could never have arrived, because every value it can reach is odd. Agents that try to swipe the
wheel instead do worse: clock.timer.set is the hardest task in our suite, passed by
one of six frontier models, and one run spent 26 turns and 228 seconds on it and produced
nothing.
Where the tap comes from
The mechanism is in Appium's WebDriverAgent, in
WebDriverAgentLib/Categories/XCUIElement+FBPickerWheel.m. The whole adjustment is
three statements: take a coordinate at normalized offset (0.5, 0.5 + relativeHeightOffset)
on the wheel, tap it, then spin the run loop until the element's value differs from
what it was, giving up after VALUE_CHANGE_TIMEOUT, which the file sets to 2 seconds.
There is no loop over rows and no check of what the tap landed on. A commit on 9 September 2026,
"rescale gesture coordinates for compatibility-mode window mismatches", moved the calculation to
normalized offsets end to end, citing appium/appium#16185.
Appium's driver documentation exposes the same thing as
mobile: selectPickerWheelValue, with offset in the range 0.01 to 0.5,
default 0.2, described as how far from the wheel's centre the click should happen, and
maxAttempts defaulting to 25. The documentation warns in the abstract about exactly
the failure we measured: too small an offset may not change the value, too high an offset "may
cause the wheel to switch two or more values at once". What it cannot tell you is which offset
does which on the screen in front of you, because that depends on the wheel's pixel height and its
row height, and both are per-screen properties.
Everything below was measured on a physical iPhone 17 Pro Max running iOS 26.6 from macOS 26.5,
through WebDriverAgent v16.12.4, and is recorded in FINDINGS.md sections 15 and 22 of
the phoneshell repository, published 15 September 2026.
The measurements
| fact | value | source |
|---|---|---|
| how a wheel is moved | one tap at normalized offset (0.5, 0.5 + offset) | WebDriverAgent XCUIElement+FBPickerWheel.m |
| wait for the value to change | 2 s, then an error | same file, VALUE_CHANGE_TIMEOUT |
| default offset, allowed range | 0.2, in [0.01, 0.5] | Appium XCUITest driver docs |
| default attempts when a value is given | 25 | Appium XCUITest driver docs |
| Clock timer wheel height | 292 pt | FINDINGS s.15 |
| rows moved per tap at offset 0.2 on that wheel | 2 | FINDINGS s.15 |
| asking for 20 minutes from 5 | 40 taps, reached 35, target unreachable | FINDINGS s.15 |
| cost of one tap, round trip | 550 ms | FINDINGS s.2 |
| cost of reading the wheel's row back | 154 ms | FINDINGS s.18 |
| rows carried by one native drag | about 9, in 1.4 s | FINDINGS s.15 |
| drag spanning more than half the wheel height | grabs nothing; 20 rows moved 0 | FINDINGS s.15 |
clock.timer.set, worst observed run | 26 turns, 228 s, no result | FINDINGS s.12 |
clock.timer.set, six-model sweep | passed by 1 of 6 | FINDINGS s.30 |
Why a swipe never converges and a tap does
A wheel advances exactly one row per tap beside the selected row, and it lands on a row because that is what the control does when tapped. A drag carries momentum, so it decelerates into an approximate position: the agent overshoots, corrects, overshoots the other way, and spends its budget narrating the correction. That is what a 240-second timeout with zero completed turns looks like from the outside. Every frontier model we have run does it, because a wheel looks like something you scroll.
The fix has to calibrate rather than assume, because the row height is a per-screen property and the offset is a fraction of the wheel. Our implementation probes offsets 0.10, 0.16, 0.24 and 0.34, each in both directions, and derives the offset that moves exactly one row from how many rows the probe actually moved. Probing both directions is not optional: a wheel already sitting on its last row cannot move further that way, and an AM/PM wheel is exactly that shape, a two-row list with PM at the end, so a downward-only probe reports it immovable every time.
None of this is obscure to people who automate iOS for a living. The standard practitioner answer to a picker wheel is the driver's own value-selection call rather than a gesture: Rahul Shetty Academy's walkthrough below drives one that way, and the argument it passes is the offset this note measures.
offset argument assumes a row height it never measures.The second way a wheel reports success it did not achieve
Reading the row back is not enough on its own, because comparing rows by their digits throws away everything else on the row. That comparison was introduced for a real reason, so that "5" would stop matching "15 min", and it was wrong for every wheel that carries letters.
| row shows | asked for | digits-only verdict | truth | source |
|---|---|---|---|---|
9:00 AM | 9:00 PM | match | different by twelve hours | FINDINGS s.22 |
Fri Oct 9 | Mon Sep 9 | match | different by a month | FINDINGS s.22 |
1 hour before | 1 day before | match | different by a day | FINDINGS s.22 |
On a combined date wheel that returns success having fired no taps at all, and the caller saves the wrong date. The rule that survives is that the numbers must agree and whatever letters were asked for must be present on the row.
What this changes if you build or evaluate phone agents
A picker is not a hard reasoning problem. It is a control with a mechanical contract that neither the model nor the default automation primitive knows, and the gap between them is where the task dies. Three consequences follow.
- Put the primitive in the harness, not in the prompt. An agent asked to reason
about a wheel burns its budget on corrections. An agent handed
set_picker("20 minutes")spends one call. Every iOS date, time, duration, country and unit control is anXCUIElementTypePickerWheel, so one calibrated implementation covers Clock, Calendar, Health, Alarms and every sign-up form that asks for a date of birth. - A benchmark item that fails mechanically is measuring your harness. Until we
built the calibrated version,
clock.timer.setwas scoring model capability on whether the model could beat a fixed offset it was never told about. That is worth remembering when reading any mobile agent leaderboard: a task that every model fails is at least as likely to be a harness gap as a capability gap. - Do not trust a fixed offset on a wheel you have not measured. 0.2 of a 292-point wheel is two rows. On a taller wheel with the same row height it is more. Appium's own guidance suggests 0.15 to 0.3 as a usual range, which is a prior, not a measurement.
Check it yourself
Open the Clock app on the Timer tab, then find the wheel and tap it through WebDriverAgent's own endpoint at the default offset, twice, reading the value in between:
git clone https://github.com/Blomega/phoneshell && cd phoneshell
bin/phoneshell up
# the wheel's element id, from the tree
curl -s "localhost:8100/source?format=json" | python3 -c \
'import json,sys;print([e for e in json.dumps(json.load(sys.stdin)).split(chr(34)) if "PickerWheel" in e][:3])'
# one adjustment at the documented default offset
curl -s -X POST localhost:8100/session/$SID/wda/pickerwheel/$UUID/select \
-H 'content-type: application/json' -d '{"order":"next","offset":0.2}'
Read the wheel's value before and after. On the iOS 26.6 Clock timer minute wheel
it moves two rows, not one. Repeat at "offset":0.10 and it moves one. The calibrated
version that does this for you is Phone.set_picker in
phoneshell/actions.py, and the probe loop is the block commented "calibrate: find the
offset that moves this wheel exactly one row". The task itself is clock.timer.set in
the public task set at
huggingface.co/datasets/blolabel/phoneshell-bench.
What would prove this wrong
- Measure the Clock timer minute wheel on iOS 26.6 at offset 0.2 and find it moving one row per tap. That would make our two-row figure a property of this device's display scaling rather than of the wheel, and the parity argument would go with it. We predict two rows on any iPhone running iOS 26.6 with the same wheel height of 292 points.
- Show a frontier model passing
clock.timer.setin four or more of six runs with no picker primitive in the tool set, before 31 December 2026. That would mean the gesture is not the blocker and the models simply needed better prompting. - A WebDriverAgent release that calibrates the offset itself, or an iOS release that reports the selected row's index, would make the whole routine unnecessary. As of v16.12.8, released 11 September 2026, the file still taps once and waits 2 seconds for any change.
Sources
- Appium WebDriverAgent, WebDriverAgentLib/Categories/XCUIElement+FBPickerWheel.m, the tap and the two second value-change timeout. https://github.com/appium/WebDriverAgent/blob/master/WebDriverAgentLib/Categories/XCUIElement%2BFBPickerWheel.m
- Appium XCUITest driver, Execute Methods reference, mobile: selectPickerWheelValue, offset range 0.01 to 0.5 with default 0.2 and maxAttempts 25. https://appium.github.io/appium-xcuitest-driver/latest/reference/execute-methods/
- appium/appium issue 16185, cited in the WebDriverAgent source for the normalized-offset rewrite of 9 September 2026. https://github.com/appium/appium/issues/16185
- phoneshell, FINDINGS.md sections 2, 12, 15, 18, 22 and 30, published 15 September 2026. https://github.com/Blomega/phoneshell/blob/main/FINDINGS.md
- phoneshell, phoneshell/actions.py, Phone.set_picker and its calibration loop. https://github.com/Blomega/phoneshell/blob/main/phoneshell/actions.py
- Appium WebDriverAgent releases, v16.12.4 on 6 September 2026 through v16.12.8 on 11 September 2026. https://github.com/appium/WebDriverAgent/releases
- phoneshell-bench, the public task set containing clock.timer.set. https://huggingface.co/datasets/blolabel/phoneshell-bench