DEV Community

Foundry
Foundry

Posted on Fully Autonomous

Our swipe passed in the logs and bounced on the glass: measuring a flick on a real iPhone

Disclosure: I'm on the team that builds Foundry, the tool this comes from. This is a debugging write-up; the link is at the bottom.

We post short-form video from a rack of real iPhones: 9 phones, 48 accounts, 1,594 posts since Aug 21. The official Instagram, TikTok and YouTube apps are driven through iOS UI automation (WebDriverAgent). Before an account posts, it also scrolls its feed for a while, which means one gesture gets sent thousands of times a day: swipe up, next video.

Earlier write-ups covered what broke first and six failures that threw no error. This one is about a single gesture. We rewrote it, ran it on a phone with the screen recorded before shipping it, and it failed in a way our own log would have called a success.

The swipe that went nowhere

The rewritten swipe drew a clean curved path from the lower part of the screen to the upper part. On the device the feed followed the finger up, then snapped back to the same video. Vertical video feeds are paged: if the finger is not moving fast enough at the moment it lifts, the page decides you changed your mind and bounces back.

The scroll routine's log could not see that. It would have recorded every one of those swipes as advanced.

Why the log was wrong

advanced came from comparing a hash of the whole screenshot before and after the swipe. A playing video changes that hash on every frame. So a bounce, which leaves you on the same video a second later in playback, logs as a successful page.

It is the same class of bug as confirming a post by a profile counter that wobbles: the signal can be caused by something other than the event. Here it was caused by time passing.

Fix: decide "next video or same video" from something only a page change can cause. We read the overlay text (caption and account name) from frames before and after. Same text, same video, no matter how many pixels changed.

Measuring instead of guessing

We recorded the phone's screen stream with wall clock timestamps, ran the rack's own scroll routine through it, and for each swipe measured the speed of the last moving segment before lift, in points per second. On an iPhone 13 Pro:

App Bounced back at Advanced from
YouTube Shorts 208 to 397 pt/s (7 swipes) 435 pt/s
Instagram Reels 261, 295 and 328 pt/s 362 pt/s
TikTok 295 pt/s not pinned down

The new swipe lifted at a median of about 600 pt/s, which clears all of that. But on the small screens, where YouTube's layout shortens the stroke, the median was 385, and the slowest swipe it could generate lifted at 156. So the typical swipe paged and the slow tail bounced. Distance was never the problem. A long, slow drag bounces; a short, quick flick pages.

There was a second defect at the other end of the stroke. The swipe started almost at rest, taking about 100 ms to move its first 10 points. TikTok ignored two of those outright when a sponsored item with a button was under the touch. A touch that sits still for a tenth of a second looks like a press, not a scroll.

The fix

Two changes to the gesture:

  • It lands already moving (32 to 44% of its top speed) and is 10 points from where it landed within 27 ms.
  • It peaks late and lifts at 86 to 95% of that peak, never under 778 pt/s (the median is 1,024 to 1,313 depending on the screen), with a total duration of 202 to 606 ms by stroke length.

We generated 48,000 swipes and checked every one against those limits before any of them touched a phone. Then the same recording harness again, on a 13 Pro and on an iPhone SE: 143 of 144 swipes went to the next video. That set includes the 12 weakest swipes per app, chosen on purpose, since the average swipe was never the one that failed. The single miss was a sponsored Reel, which can take two swipes for a person too.

What we changed in how we test gestures

  1. Prove a gesture from a recording, not from the automation's return value. WebDriverAgent will happily report that it performed the touch actions. It cannot tell you what the app did with them.
  2. Test the weakest case, not the typical one. Sort your generated swipes by lift speed and send the slowest dozen.
  3. Test the smallest screen you support and a big one. Our rack mixes 375x667, 375x812 and 390x844 points. A stroke scaled to the screen height has a different speed on each.
  4. Keep a number per app. The thresholds above are from our recordings in October 2026. Apps change. The harness is what we keep, not the numbers.

A second silent one, found on the way

While reading the device session logs for this we noticed our "recycle each device session after 60 minutes" rule had never fired. It read the process age with ps -o etimes=, which Linux has and macOS does not. The command failed quietly, the age was never over the limit, and sessions had been running 15 hours and more.

Nothing threw. It is always the check that runs fine and measures the wrong thing.

If you drive real devices

Whatever your stack (WebDriverAgent, Appium, XCUITest), the question that paid for itself was: could this check pass if the thing I care about did not happen? For our swipe log the answer was yes, on every frame of every video.

The rack and the agent CLI are what we build at Foundry. Happy to answer questions about the harness in the comments.

Top comments (0)