Skip to content

fix: swipes that start on press-* elements don't scroll (iOS) - #524

Open
SRWieZ wants to merge 1 commit into
NativePHP:mainfrom
SRWieZ:fix/ios-press-feedback-scroll-public
Open

SRWieZ wants to merge 1 commit into
NativePHP:mainfrom
SRWieZ:fix/ios-press-feedback-scroll-public

Conversation

@SRWieZ

@SRWieZ SRWieZ commented Oct 3, 2026 •

Copy link
Copy Markdown
Member

Follow-up to #394, this time without any private API.

What's wrong

On iOS, a swipe that starts on an element with press-scale, press-opacity or press-translate-y doesn't scroll. On a screen full of pressable cards, you can only scroll by aiming at the gaps between them.

Reproduced on an iPhone 16 Plus running iOS 27.0.1, so it isn't limited to old iOS versions.

Why

To know when a finger is down, the press effect listens for a "drag" that starts at zero distance. iOS hands the touch to that drag first, so the scroll view never gets it.

What this does

It swaps that drag for iOS's standard touch listener (a long-press recognizer with a 0 s duration) that only watches the finger: it never takes the touch away from the scroll view, @press, @tap or long-press. The effect goes away when the finger lifts, or once it moves far enough to be a scroll.

  • Public Apple API only (UIGestureRecognizerRepresentable, iOS 18+; the app's minimum is already 18.2)
  • One Swift file. Android is untouched.

How to reproduce

image

Four kinds of cards: pick one at the top, then start a swipe on a card.

  • Before: only "no press-*" scrolls.
  • After: all four scroll, and taps still count.
// routes/mobile.php
Route::native('/repro/press-scroll', \App\NativeComponents\PressScroll::class);
// app/NativeComponents/PressScroll.php
namespace App\NativeComponents;

use Illuminate\View\View;
use Native\Mobile\Edge\NativeComponent;

class PressScroll extends NativeComponent
{
    public string $variant = 'scale-press';

    public int $taps = 0;

    public function pick(string $variant): void
    {
        $this->variant = $variant;
        $this->taps = 0;
    }

    public function tapped(): void
    {
        $this->taps++;
    }

    public function render(): View
    {
        return view('native.press-scroll');
    }
}
{{-- resources/views/native/press-scroll.blade.php --}}
@php
    $variants = [
        'scale-press' => 'scale + @press',
        'scale-only' => 'scale, no @press',
        'opacity-press' => 'opacity + @press',
        'plain-press' => 'no press-*',
    ];
@endphp
<native:column class="w-full h-full bg-zinc-100">
    <native:top-bar title="Press + scroll" />

    <native:column class="w-full px-4 pt-3 pb-2 gap-2">
        <native:row class="w-full gap-2">
            @foreach ($variants as $key => $label)
                <native:pressable @press="pick('{{ $key }}')"
                    class="flex-1 h-[44] rounded-xl items-center justify-center {{ $variant === $key ? 'bg-blue-600' : 'bg-white' }}">
                    <native:text class="text-[12] font-semibold {{ $variant === $key ? 'text-white' : 'text-zinc-800' }}">{{ $label }}</native:text>
                </native:pressable>
            @endforeach
        </native:row>
        <native:text class="text-[14] text-zinc-600">Taps: {{ $taps }}</native:text>
    </native:column>

    <native:scroll-view class="w-full flex-1">
        <native:column class="w-full p-4 gap-3">
            @for ($i = 1; $i <= 24; $i++)
                @if ($variant === 'scale-press')
                    <native:pressable :press-scale="0.97" @press="tapped" class="w-full h-[80] rounded-2xl bg-white items-center justify-center">
                        <native:text class="text-[17] font-semibold">Item {{ $i }}</native:text>
                    </native:pressable>
                @elseif ($variant === 'scale-only')
                    <native:pressable :press-scale="0.97" class="w-full h-[80] rounded-2xl bg-white items-center justify-center">
                        <native:text class="text-[17] font-semibold">Item {{ $i }}</native:text>
                    </native:pressable>
                @elseif ($variant === 'opacity-press')
                    <native:pressable :press-opacity="0.6" @press="tapped" class="w-full h-[80] rounded-2xl bg-white items-center justify-center">
                        <native:text class="text-[17] font-semibold">Item {{ $i }}</native:text>
                    </native:pressable>
                @else
                    <native:pressable @press="tapped" class="w-full h-[80] rounded-2xl bg-white items-center justify-center">
                        <native:text class="text-[17] font-semibold">Item {{ $i }}</native:text>
                    </native:pressable>
                @endif
            @endfor
        </native:column>
    </native:scroll-view>
</native:column>

…eep scrolling

A zero-distance DragGesture attached with simultaneousGesture claims the
touch before an enclosing ScrollView can start panning, so a swipe that
starts on any element with press-scale / press-opacity / press-translate-y
never scrolls (reproduced on iOS 27.0.1). Press state now comes from a
zero-duration UILongPressGestureRecognizer bridged through
UIGestureRecognizerRepresentable (public since iOS 18, the deployment
target is 18.2): it recognizes simultaneously with every gesture, never
cancels or delays touches, and releases the press on lift, on cancel, or
once the finger has moved past tap slop.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant