Blolabel
← Notes
foundational 8 min read

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.

Abstract technical illustration of a vertical cylinder of evenly spaced bands with a single bright point of contact above its midline, and a blurred momentum arc overshooting past it

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

factvaluesource
how a wheel is movedone tap at normalized offset (0.5, 0.5 + offset)WebDriverAgent XCUIElement+FBPickerWheel.m
wait for the value to change2 s, then an errorsame file, VALUE_CHANGE_TIMEOUT
default offset, allowed range0.2, in [0.01, 0.5]Appium XCUITest driver docs
default attempts when a value is given25Appium XCUITest driver docs
Clock timer wheel height292 ptFINDINGS s.15
rows moved per tap at offset 0.2 on that wheel2FINDINGS s.15
asking for 20 minutes from 540 taps, reached 35, target unreachableFINDINGS s.15
cost of one tap, round trip550 msFINDINGS s.2
cost of reading the wheel's row back154 msFINDINGS s.18
rows carried by one native dragabout 9, in 1.4 sFINDINGS s.15
drag spanning more than half the wheel heightgrabs nothing; 20 rows moved 0FINDINGS s.15
clock.timer.set, worst observed run26 turns, 228 s, no resultFINDINGS s.12
clock.timer.set, six-model sweeppassed by 1 of 6FINDINGS s.30
Two rows per tap cannot reach an even minute from an odd one iOS 26.6 Clock timer wheel, 292 pt tall, WebDriverAgent default offset 0.2 5152535 minutes on the wheel target: 20 minutes, never touched 5913171921252933 wheel value after each tap, first tap on the left The wheel steps 19 to 21 without stopping at 20. Forty taps later the run gave up on 35, having never been wrong about any single tap.
The failure is parity, not precision. A fixed offset that happens to move two rows makes half the wheel unreachable, and nothing in the stack reports that.

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.

Three ways to move a wheel, one of which lands WebDriverAgent default 1718 19 2021 tap here 0.2 x 292 pt lands 2 rows below, so the wheel steps 19 to 21 and skips 20 waits up to 2 s for any change what agents try: drag momentum lands approximately, so it overshoots and corrects 26 turns, 228 s, no result calibrate, then arithmetic 1. probe 0.10 / 0.16 / 0.24 / 0.34 both directions, every time 2. rows moved -> offset for 1 row 3. more than 5 rows away: drag about 9 rows in 1.4 s, clamped to half the wheel's height 4. remaining rows = arithmetic fire them, read once at the end 5. match digits AND letters tap 550 ms, read 154 ms, so the expensive thing is taps, not looking 9 taps cost 5 s; one drag costs 1.4 s Every iOS date, time, duration, country and unit control is an XCUIElementTypePickerWheel, so one calibrated primitive covers Clock, Calendar, Health and every date-of-birth form.
The default assumes a row height. The agent assumes a scroll. Only the calibrated routine measures the wheel it is actually looking at.

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.

"Appium IOS Automation Part 5: Automate Picker Wheels (Dropdowns)", @RahulShettyAcademy, 20 December 2020. Supports the claim that a picker wheel is handled through the driver's value-selection call, whose offset argument assumes a row height it never measures.
Moving nine rows, three ways Arithmetic on measured unit costs: 550 ms per tap, 154 ms per tree read, 1,400 ms per native drag 9 taps, read once at the end 9 taps, a read after each one native drag 4.95 s6.34 s1.40 s 02 s4 s6 s8 s wall clock to cover nine rows
Why the routine drags for distance and taps for the last few rows. The middle bar is the naive loop that reads the wheel after every single tap.

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 showsasked fordigits-only verdicttruthsource
9:00 AM9:00 PMmatchdifferent by twelve hoursFINDINGS s.22
Fri Oct 9Mon Sep 9matchdifferent by a monthFINDINGS s.22
1 hour before1 day beforematchdifferent by a dayFINDINGS 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.

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

Sources

  1. 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
  2. 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/
  3. 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
  4. phoneshell, FINDINGS.md sections 2, 12, 15, 18, 22 and 30, published 15 September 2026. https://github.com/Blomega/phoneshell/blob/main/FINDINGS.md
  5. phoneshell, phoneshell/actions.py, Phone.set_picker and its calibration loop. https://github.com/Blomega/phoneshell/blob/main/phoneshell/actions.py
  6. Appium WebDriverAgent releases, v16.12.4 on 6 September 2026 through v16.12.8 on 11 September 2026. https://github.com/appium/WebDriverAgent/releases
  7. phoneshell-bench, the public task set containing clock.timer.set. https://huggingface.co/datasets/blolabel/phoneshell-bench