flutter icon indicating copy to clipboard operation
flutter copied to clipboard

[iPadOS 26] Clicking the top edge of the screen is recognized twice

Open MinSeungHyun opened this issue 6 months ago • 11 comments

Steps to reproduce

  1. Run any Flutter app on iPad (iOS 26). (I couldn't reproduce on iPhone)
  2. Change SystemUiMode to immersive by running SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);.
  3. You might need to rotate the screen to apply immersive mode due to this issue. #175520
  4. Click the top edge of the screen, and you can notice that the click is recognized twice.

Expected results

A single click should be recognized once.

Actual results

A single click is recognized twice.

https://github.com/user-attachments/assets/fb48a0b0-ce38-419f-8894-d466c1f955b2

Code sample

Code sample
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _count = 0;

  @override
  void initState() {
    super.initState();
    SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: GestureDetector(
          onTap: () {
            setState(() {
              _count++;
            });
          },
          child: Container(
            width: double.infinity,
            height: double.infinity,
            color: Colors.grey,
            alignment: Alignment.topCenter,
            child: Text(
              '$_count',
              style: const TextStyle(
                fontSize: 40,
                fontWeight: FontWeight.bold,
                color: Colors.black,
              ),
            ),
          ),
        ),
      ),
    );
  }
}

Screenshots or Video

No response

Logs

No response

Flutter Doctor output

Doctor output
[✓] Flutter (Channel stable, 3.35.4, on macOS 26.0 25A354 darwin-arm64, locale en-KR) [243ms]
    • Flutter version 3.35.4 on channel stable at /Users/seunghyun/fvm/versions/stable
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision d693b4b9db (2 days ago), 2025-09-16 14:27:41 +0000
    • Engine revision c298091351
    • Dart version 3.9.2
    • DevTools version 2.48.0
    • Feature flags: enable-web, no-enable-linux-desktop, no-enable-macos-desktop, no-enable-windows-desktop,
      enable-android, enable-ios, cli-animations, enable-lldb-debugging

[✓] Android toolchain - develop for Android devices (Android SDK version 35.0.0) [997ms]
    • Android SDK at /Users/seunghyun/Library/Android/Sdk
    • Emulator version 35.2.10.0 (build_id 12414864) (CL:N/A)
    • Platform android-35, build-tools 35.0.0
    • ANDROID_HOME = /Users/seunghyun/Library/Android/Sdk
    • Java binary at: /Users/seunghyun/Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
      This is the JDK bundled with the latest Android Studio installation on this machine.
      To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`.
    • Java version OpenJDK Runtime Environment (build 21.0.6+-13368085-b895.109)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 26.0) [625ms]
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 17A324
    • CocoaPods version 1.16.2

[✓] Chrome - develop for the web [18ms]
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2024.3) [18ms]
    • Android Studio at /Users/seunghyun/Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 21.0.6+-13368085-b895.109)

[✓] IntelliJ IDEA Ultimate Edition (version 2025.1.2) [17ms]
    • IntelliJ at /Users/seunghyun/Applications/IntelliJ IDEA Ultimate.app
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart

[✓] VS Code (version 1.101.1) [5ms]
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.112.0

[✓] Connected device (5 available) [5.8s]
    • Seunghyun’s iPad (wireless) (mobile)         • 00008020-001B59413A83002E            • ios            • iOS 18.6.2
      22G100
    • iPhone 14 Pro (mobile)                       • 3794BC81-1DEA-4AF4-B5FA-3DA2FCF87C6F • ios            •
      com.apple.CoreSimulator.SimRuntime.iOS-26-0 (simulator)
    • iPad Pro (11-inch) (4th generation) (mobile) • 36564CCB-3F65-4FBF-8956-495C518CA3DE • ios            •
      com.apple.CoreSimulator.SimRuntime.iOS-26-0 (simulator)
    • iPad Pro (11-inch) (4th generation) (mobile) • 1E2A25BD-1080-47CF-82C6-831C4928805A • ios            •
      com.apple.CoreSimulator.SimRuntime.iOS-18-6 (simulator)
    • Chrome (web)                                 • chrome                               • web-javascript • Google Chrome
      140.0.7339.185

[✓] Network resources [352ms]
    • All expected network resources are available.

• No issues found!

MinSeungHyun avatar Sep 18 '25 16:09 MinSeungHyun

Thanks for the report. Seeing the same behaviour with latest SDK versions on iPad with iPadOS 26.

stable : 3.35.4
master : 3.37.0-1.0.pre-198

https://github.com/user-attachments/assets/f0611764-4541-4e1f-b0ef-38a6365d1c06

tirth-patel-nc avatar Sep 19 '25 10:09 tirth-patel-nc

Can I know when or in what version it will be fixed?

MinSeungHyun avatar Sep 22 '25 15:09 MinSeungHyun

I found a workaround. The second tap's position always seems to be (0, 0), so putting a 1x1 AbsorbPointer absorbs the second tap. It's a bit annoying but it works.

https://github.com/user-attachments/assets/6912a6b0-c0bc-4ba2-a83c-d0413bb80f5e

Code sample

Code sample
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _count = 0;

  @override
  void initState() {
    super.initState();
    SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Stack(
          children: [
            FilledButton(
              onPressed: () {
                setState(() {
                  print('Button pressed. count: $_count');
                  _count++;
                });
              },
              child: Text('$_count'),
            ),
            GestureDetector(
              onTapDown: (details) {
                print('Tap absorbed. position: ${details.globalPosition}');
              },
              child: AbsorbPointer(child: SizedBox(width: 1, height: 1)),
            ),
          ],
        ),
      ),
    );
  }
}

MinSeungHyun avatar Sep 22 '25 16:09 MinSeungHyun

I suspect this may be related to the new iPadOS 26 feature where double tapping the top of an app will toggle between fullscreen & windowed mode.

This workaround seems to do the trick until it's fixed properly:

class FilteringFlutterBinding extends WidgetsFlutterBinding {
  @override
  void handlePointerEvent(PointerEvent event) {
    if (event.position == Offset.zero) {
      return;
    }
    super.handlePointerEvent(event);
  }
}

void main() {
  FilteringFlutterBinding();
  runApp(MyApp());
}

Jon-Salmon avatar Oct 27 '25 21:10 Jon-Salmon

This issue is getting worse because the double tapping area @Jon-Salmon mentioned seems bigger on iPadOS 26.1.

@Jon-Salmon's workaround is working for me. Thanks.

MinSeungHyun avatar Oct 29 '25 07:10 MinSeungHyun

This is a breaking changes for all of the existing iPad apps already in store. Any app that has hamburger menu on top header are running into issue where a single tap is recognized as double tap and it opens and shuts the menu right away. Same thing with any dialogs or side sheets. Button clicks unless debounced are registered as double clicks and then the action happens twice. This is not an inconvenience. This is a major flaw that affects all the apps existing on the store for the customers upgrading to iPad OS 26.0 and gets worse in 26.1.

Edit: @Jon-Salmon's solution didn't work for me. However this worked:

// Top level global 
bool _zeroOffsetPointerGuardInstalled = false;

void _installZeroOffsetPointerGuard() {
  if (_zeroOffsetPointerGuardInstalled) return;
  GestureBinding.instance.pointerRouter
      .addGlobalRoute(_absorbZeroOffsetPointerEvent);
  _zeroOffsetPointerGuardInstalled = true;
}

void _absorbZeroOffsetPointerEvent(PointerEvent event) {
  if (event.position == Offset.zero) {
    GestureBinding.instance.cancelPointer(event.pointer);
  }
}

And on the main method:

void main(){
 WidgetsFlutterBinding.ensureInitialized();
  _installZeroOffsetPointerGuard();
runApp(const App());
}

ssbigyan avatar Nov 25 '25 15:11 ssbigyan

The offending code is in: https://github.com/flutter/engine/blob/master_archived/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm

- (BOOL)scrollViewShouldScrollToTop:(UIScrollView*)scrollView {
  if (!_engine) {
    return NO;
  }
  CGPoint statusBarPoint = CGPointZero;
  UIScreen* screen = [self flutterScreenIfViewLoaded];
  if (screen) {
    SendFakeTouchEvent(screen, _engine.get(), statusBarPoint, flutter::PointerData::Change::kDown);
    SendFakeTouchEvent(screen, _engine.get(), statusBarPoint, flutter::PointerData::Change::kUp);
  }
  return NO;
}

It looks like iPadOS 26.1 is triggering scrollViewShouldScrollToTop further down into the navigation bar.

This issue even affects the latest version of Chrome on iPadOS 26.1 according to this reddit post.

@Jon-Salmon's workaround is good but stops the legitimate scroll to top behavior when tapping the actual status bar. I've been playing around with a variation that tries to distinguish these fake taps and ignore them. It might be useful for someone:

class FilteringFlutterBinding extends WidgetsFlutterBinding {
  // Track timestamp of last real (non-zero) pointer up event
  DateTime? _lastRealPointerUpTime;
  static const int _fakeTapThreshold = 400;

  @override
  void handlePointerEvent(PointerEvent event) {
    // Track when real taps (non-zero position) complete
    if (event is PointerUpEvent && event.position != Offset.zero) {
      _lastRealPointerUpTime = DateTime.now();
    }

    // Filter fake taps: Offset.zero down events that follow shortly after a real tap
    if (event.position == Offset.zero && event is PointerDownEvent) {
      if (_lastRealPointerUpTime != null) {
        final timeSinceRealTap = DateTime.now().difference(_lastRealPointerUpTime!).inMilliseconds;
        if (timeSinceRealTap < _fakeTapThreshold) {
          return;
        }
      }
    }

    // Also block the matching PointerUpEvent for a blocked PointerDownEvent
    if (event.position == Offset.zero && event is PointerUpEvent) {
      if (_lastRealPointerUpTime != null) {
        final timeSinceRealTap = DateTime.now().difference(_lastRealPointerUpTime!).inMilliseconds;
        if (timeSinceRealTap < _fakeTapThreshold) {
          return;
        }
       
      }
    }

    super.handlePointerEvent(event);
  }
} 

enhancient avatar Nov 25 '25 21:11 enhancient

Cross-listing https://github.com/flutter/flutter/issues/35050, let's make sure that isn't regressed with this fix.

jmagman avatar Dec 01 '25 22:12 jmagman

I am affected by this too, should we workaround or wait for Apple to fix this? any thoughts or ideas?

saif-ellafi avatar Dec 09 '25 18:12 saif-ellafi

Since this seems to have the same root cause as https://github.com/flutter/flutter/issues/177992, I'll post my updates in the other issue (Although I did not try to reproduce this issue on master, it's a bit strange that UIKit actually dispatches the touch events since I would expect it be consumed by the scroll view).

LongCatIsLooong avatar Dec 09 '25 18:12 LongCatIsLooong

I chose the workaround suggested by @Jon-Salmon here (https://github.com/flutter/flutter/issues/175606#issuecomment-3453392532) It worked for me!

cristianms avatar Dec 09 '25 19:12 cristianms

I can confirm that my production app is also affected as it has a leading hamburger PopUpMenuButton in its appBar. The solution proposed @ssbigyan here seems to work for me for now (https://github.com/flutter/flutter/issues/175606#issuecomment-3576240885).

les-larsj avatar Dec 14 '25 11:12 les-larsj