New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
iOS App Freeze for ~0.5 second on Swipe Back Gesture #48225
Comments
I'm facing the same problem. @edwardez Were you able to find a workaround or we should wait for an answer from the Flutter team? |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Hi @edwardez logs[✓] Flutter (Channel master, 1.20.0-8.0.pre.40, on Mac OS X 10.15.5 19F101, locale en-GB)
• Flutter version 1.20.0-8.0.pre.40 at /Users/nevercode/development/flutter_master
• Framework revision 6eaaf1650e (10 hours ago), 2020-07-09 18:04:37 -0700
• Engine revision 9b3e3410f0
• Dart version 2.9.0 (build 2.9.0-20.0.dev 06cb010247)
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
• Android SDK at /Users/nevercode/Library/Android/sdk
• Platform android-29, build-tools 29.0.2
• Java binary at: /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java
• Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6222593)
• All Android licenses accepted.
[✓] Xcode - develop for iOS and macOS (Xcode 11.5)
• Xcode at /Applications/Xcode.app/Contents/Developer
• Xcode 11.5, Build version 11E608c
• CocoaPods version 1.9.0
[✓] Chrome - develop for the web
• Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
[✓] Android Studio (version 4.0)
• Android Studio at /Applications/Android Studio.app/Contents
• Flutter plugin version 46.0.2
• Dart plugin version 193.7361
• Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6222593)
[✓] VS Code
• VS Code at /Applications/Visual Studio Code.app/Contents
• Flutter extension version 3.12.1
[✓] Connected device (4 available)
• iPhone 11 Pro Max (mobile) • D6967F45-CB0F-4D4E-AFD5-278816A21CAE • ios • com.apple.CoreSimulator.SimRuntime.iOS-13-5 (simulator)
• macOS (desktop) • macos • darwin-x64 • Mac OS X 10.15.5 19F101
• Web Server (web) • web-server • web-javascript • Flutter Tools
• Chrome (web) • chrome • web-javascript • Google Chrome 83.0.4103.116
• No issues found! I cannot reproduce the behaviour you are describing |
I'm facing the same problem.
and go back using swipe back gesture, UI is not clickable for like 0.5-1 second. I added back button ( tested on simulator and real device - same behaviour |
Hi @palicka @ldelafuente |
Without additional information, we are unfortunately not sure how to resolve this issue. Could everyone who still has this problem please file a new issue with the exact description of what happens, logs, and the output of Thanks for your contribution. |
I believe this issue is still there as of Flutter stable 1.22, minimal code/recorded video are all in the bug, not sure if I can provide any additional info. But, if Flutter maintainers cannot reproduce this issue, probably it's just because I'm a little bit picky here. |
The thread ended on a question for more information and it's standard practice to close if it isn't provided in 21 days (the bot should actually do this). I was able to reproduce this on the latest master flutter doctor -v
Not sure if this is intended as a failsafe of some sort for the gesture but it does happen. The steps can be simplified to:
This is observable on both physical and iOS 14 simulator. |
I'm facing the same problem on all of my 10+ apps. flutter doctor -v
|
Any updates on this @markusaksli-nc ? Has anyone found a track or a workaround ? :) |
I got a 500 point bounty on this: if anyone is interested in racking up some points :) |
I tried investigating this issue using the sample code provided by @edwardez and found out that this is due to... File: routes.dart
Setting the When I tried setting the File: navigator.dart
This also resolves the issue but there's no animation for the First screen. (same as pressing back button in AppBar). My Flutter doctor...
|
Having this issue too in iOS in every screen. Is there any news on this? Anyone found a solution/workaround? |
Is there any way to fix it now? |
I found a hack 😏 import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
void main() => runApp(const AppRoot());
@immutable
class CustomNavigator extends Navigator {
const CustomNavigator({
Key? key,
List<Page<dynamic>> pages = const <Page<dynamic>>[],
PopPageCallback? onPopPage,
TransitionDelegate<dynamic> transitionDelegate =
const DefaultTransitionDelegate<dynamic>(),
String? initialRoute,
RouteFactory? onGenerateRoute,
RouteFactory? onUnknownRoute,
List<NavigatorObserver> observers = const <NavigatorObserver>[],
String? restorationScopeId,
RouteListFactory onGenerateInitialRoutes =
Navigator.defaultGenerateInitialRoutes,
bool reportsRouteUpdateToEngine = false,
bool requestFocus = true,
}) : super(
key: key,
pages: pages,
onPopPage: onPopPage,
transitionDelegate: transitionDelegate,
initialRoute: initialRoute,
onGenerateRoute: onGenerateRoute,
onUnknownRoute: onUnknownRoute,
observers: observers,
restorationScopeId: restorationScopeId,
onGenerateInitialRoutes: onGenerateInitialRoutes,
reportsRouteUpdateToEngine: reportsRouteUpdateToEngine,
requestFocus: requestFocus,
);
@override
CustomNavigatorState createState() => CustomNavigatorState();
}
class CustomNavigatorState extends NavigatorState {
bool _originalTimingGestureInProgress = false;
bool get originalTimingGestureInProgress => _originalTimingGestureInProgress;
bool _gestureInProgress = false;
@override
bool get userGestureInProgress => _gestureInProgress;
@override
void didStartUserGesture() {
_gestureInProgress = true;
_originalTimingGestureInProgress = true;
super.didStartUserGesture();
}
@override
void didStopUserGesture() {
_gestureInProgress = false;
_originalTimingGestureInProgress = false;
super.didStopUserGesture();
}
void _handlePointerEnd() => _gestureInProgress = false;
@override
Widget build(BuildContext context) {
final child = super.build(context);
return Listener(
onPointerUp: (event) => _handlePointerEnd(),
onPointerCancel: (event) => _handlePointerEnd(),
child: child,
);
}
}
@immutable
class CustomPageTransitionsTheme extends PageTransitionsTheme {
const CustomPageTransitionsTheme();
static const builder = CupertinoPageTransitionsBuilder();
@override
Widget buildTransitions<T>(
PageRoute<T> route,
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
final originalPageTransition = builder.buildTransitions<T>(
route,
context,
animation,
secondaryAnimation,
child,
);
final linearTransition = (route.navigator! as CustomNavigatorState)
.originalTimingGestureInProgress;
if (route.fullscreenDialog) {
return CupertinoFullscreenDialogTransition(
primaryRouteAnimation: animation,
secondaryRouteAnimation: secondaryAnimation,
linearTransition: linearTransition,
child: child,
);
} else {
return CupertinoPageTransition(
primaryRouteAnimation: animation,
secondaryRouteAnimation: secondaryAnimation,
linearTransition: linearTransition,
child: (originalPageTransition as CupertinoPageTransition).child,
);
}
}
}
@immutable
class AppRoot extends StatelessWidget {
const AppRoot({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
pageTransitionsTheme: const CustomPageTransitionsTheme(),
),
home: const SamplePage(),
builder: (context, child) {
final navigator = child! as Navigator;
return CustomNavigator(
key: navigator.key,
pages: navigator.pages,
onPopPage: navigator.onPopPage,
transitionDelegate: navigator.transitionDelegate,
initialRoute: navigator.initialRoute,
onGenerateRoute: navigator.onGenerateRoute,
onUnknownRoute: navigator.onUnknownRoute,
observers: navigator.observers,
restorationScopeId: navigator.restorationScopeId,
onGenerateInitialRoutes: navigator.onGenerateInitialRoutes,
reportsRouteUpdateToEngine: navigator.reportsRouteUpdateToEngine,
requestFocus: navigator.requestFocus,
);
},
);
}
}
@immutable
class SamplePage extends StatelessWidget {
const SamplePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Pop gesture hack sample')),
body: ListView.builder(
itemBuilder: (context, index) {
return ListTile(
title: Text('$index'),
subtitle: const Text('Tap to push page'),
onTap: () {
Navigator.push<void>(
context,
MaterialPageRoute(builder: (context) => const SamplePage()),
);
},
);
},
),
);
}
} |
When will this be fixed it is really frustrating, it breaks user experience on IOS, user mostly use swipe back gesture to go to previous screen. this also happens when using Dismissible. |
VID_20220525_100411.mp4 |
I have same problem in flutter 3 |
@willlockwood are there any news on this fix, I see this issue on flutter 3.x.x also. This issue really affects user experience on IOS, users perceive app as not responsive when they swipe back, this is bigger issue than IOS jank. |
Just came accross this issue when developing the very basics of our new app, and it's really not ideal in terms of user experience. A simple app that should have no performance issues just feels like it's been badly built. Is there any update on creative ways around it? |
It's a quite painful UX issue; 'swipe to navigate back to a list ' is ubiquitous user behavior, it's not like this is some edge case that's been overlooked. This issue has been open for nearly 3 years, so I have no expectation that it will ever be fixed. Unfortunately, due to this, my team will likely need to move to a different stack for our upcoming project. |
I think they don't look at these old issues, maybe open a new issue then they will a least have to reply. |
@toda-bps you're a life saver! thank god!! Amazing job on the hack! I'm a iOS developer of 5+ years experience that started building a production app with Flutter. We're launching our startup app in a couple of weeks. This 0.5 freezing behavior that occurs after the swipe back gestures on iOS is still here. I confirm. I hope the flutter devs fix this soon! It looks like the issue has been here for 3+ years. I almost considered switching to React Native😭 Other than that, I really enjoy Flutter and it's potential! But in the meanwhile, the hack works perfectly! even with the GetX library's navigation! Big thanks! |
Same issue happens to me with both XS Max and 11Pro on iOS 16.2. `flutter doctor -v [✓] Android toolchain - develop for Android devices (Android SDK version 33.0.0) [✓] Xcode - develop for iOS and macOS (Xcode 14.2) [✓] Chrome - develop for the web [✓] Android Studio (version 2021.3) [✓] Connected device (4 available) [✓] HTTP Host Availability • No issues found! |
The hack doesn't seem to work anymore. The child is no longer a navigator but a FocusScope. Anyone have a solution for replacing the navigator to fix this problem? |
@scottandrewzip I found that FocusScope's child is the Navigator. I updated the hack: Tested with 3.3.10, 3.7.3 and 3.8.0-9.0.pre.20(001c495) which is the latest at the moment. import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
void main() => runApp(const AppRoot());
@immutable
class CustomNavigator extends Navigator {
const CustomNavigator({
super.key,
super.pages,
super.onPopPage,
super.initialRoute,
super.onGenerateInitialRoutes,
super.onGenerateRoute,
super.onUnknownRoute,
super.transitionDelegate,
super.reportsRouteUpdateToEngine,
// >= 3.7.0
// super.clipBehavior,
super.observers,
super.requestFocus,
super.restorationScopeId,
});
@override
CustomNavigatorState createState() => CustomNavigatorState();
}
class CustomNavigatorState extends NavigatorState {
bool _originalTimingGestureInProgress = false;
bool get originalTimingGestureInProgress => _originalTimingGestureInProgress;
bool _gestureInProgress = false;
@override
bool get userGestureInProgress => _gestureInProgress;
@override
void didStartUserGesture() {
_gestureInProgress = true;
_originalTimingGestureInProgress = true;
super.didStartUserGesture();
}
@override
void didStopUserGesture() {
_gestureInProgress = false;
_originalTimingGestureInProgress = false;
super.didStopUserGesture();
}
void _handlePointerEnd() => _gestureInProgress = false;
@override
Widget build(BuildContext context) {
final child = super.build(context);
return Listener(
onPointerUp: (event) => _handlePointerEnd(),
onPointerCancel: (event) => _handlePointerEnd(),
child: child,
);
}
}
@immutable
class CustomPageTransitionsTheme extends PageTransitionsTheme {
const CustomPageTransitionsTheme();
static const _builder = CupertinoPageTransitionsBuilder();
@override
Widget buildTransitions<T>(
PageRoute<T> route,
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
final originalPageTransition = _builder.buildTransitions<T>(
route,
context,
animation,
secondaryAnimation,
child,
);
final navigator = route.navigator;
if (navigator is! CustomNavigatorState) {
return originalPageTransition;
}
final linearTransition = navigator.originalTimingGestureInProgress;
if (route.fullscreenDialog) {
return CupertinoFullscreenDialogTransition(
primaryRouteAnimation: animation,
secondaryRouteAnimation: secondaryAnimation,
linearTransition: linearTransition,
child: child,
);
} else {
return CupertinoPageTransition(
primaryRouteAnimation: animation,
secondaryRouteAnimation: secondaryAnimation,
linearTransition: linearTransition,
child: (originalPageTransition as CupertinoPageTransition).child,
);
}
}
}
@immutable
class AppRoot extends StatelessWidget {
const AppRoot({super.key});
Navigator _findNavigator(dynamic child) {
if (child is Navigator) {
return child;
}
// ignore: avoid_dynamic_calls
return _findNavigator(child.child);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
pageTransitionsTheme: const CustomPageTransitionsTheme(),
),
home: const SamplePage(),
builder: (context, child) {
final navigator = _findNavigator(child);
return CustomNavigator(
key: navigator.key,
pages: navigator.pages,
onPopPage: navigator.onPopPage,
initialRoute: navigator.initialRoute,
onGenerateInitialRoutes: navigator.onGenerateInitialRoutes,
onGenerateRoute: navigator.onGenerateRoute,
onUnknownRoute: navigator.onUnknownRoute,
transitionDelegate: navigator.transitionDelegate,
reportsRouteUpdateToEngine: navigator.reportsRouteUpdateToEngine,
// >= 3.7.0
// clipBehavior: navigator.clipBehavior,
observers: navigator.observers,
requestFocus: navigator.requestFocus,
restorationScopeId: navigator.restorationScopeId,
);
},
);
}
}
@immutable
class SamplePage extends StatelessWidget {
const SamplePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Pop gesture hack sample')),
body: ListView.builder(
itemBuilder: (context, index) {
return ListTile(
title: Text('$index'),
subtitle: const Text('Tap to push page'),
onTap: () {
unawaited(
Navigator.push<void>(
context,
MaterialPageRoute(builder: (context) => const SamplePage()),
),
);
},
);
},
),
);
}
} |
Another example: using go_router with ShellRoute. import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
void main() => runApp(const AppRoot());
@immutable
class CustomNavigator extends Navigator {
const CustomNavigator({
super.key,
super.pages,
super.onPopPage,
super.initialRoute,
super.onGenerateInitialRoutes,
super.onGenerateRoute,
super.onUnknownRoute,
super.transitionDelegate,
super.reportsRouteUpdateToEngine,
// >= 3.7.0
// super.clipBehavior,
super.observers,
super.requestFocus,
super.restorationScopeId,
});
@override
CustomNavigatorState createState() => CustomNavigatorState();
}
class CustomNavigatorState extends NavigatorState {
bool _originalTimingGestureInProgress = false;
bool get originalTimingGestureInProgress => _originalTimingGestureInProgress;
bool _gestureInProgress = false;
@override
bool get userGestureInProgress => _gestureInProgress;
@override
void didStartUserGesture() {
_gestureInProgress = true;
_originalTimingGestureInProgress = true;
super.didStartUserGesture();
}
@override
void didStopUserGesture() {
_gestureInProgress = false;
_originalTimingGestureInProgress = false;
super.didStopUserGesture();
}
void _handlePointerEnd() => _gestureInProgress = false;
@override
Widget build(BuildContext context) {
final child = super.build(context);
return Listener(
onPointerUp: (event) => _handlePointerEnd(),
onPointerCancel: (event) => _handlePointerEnd(),
child: child,
);
}
}
@immutable
class CustomPageTransitionsTheme extends PageTransitionsTheme {
const CustomPageTransitionsTheme();
static const _builder = CupertinoPageTransitionsBuilder();
@override
Widget buildTransitions<T>(
PageRoute<T> route,
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
final originalPageTransition = _builder.buildTransitions<T>(
route,
context,
animation,
secondaryAnimation,
child,
);
final navigator = route.navigator;
if (navigator is! CustomNavigatorState) {
return originalPageTransition;
}
final linearTransition = navigator.originalTimingGestureInProgress;
if (route.fullscreenDialog) {
return CupertinoFullscreenDialogTransition(
primaryRouteAnimation: animation,
secondaryRouteAnimation: secondaryAnimation,
linearTransition: linearTransition,
child: child,
);
} else {
return CupertinoPageTransition(
primaryRouteAnimation: animation,
secondaryRouteAnimation: secondaryAnimation,
linearTransition: linearTransition,
child: (originalPageTransition as CupertinoPageTransition).child,
);
}
}
}
Navigator _findNavigator(dynamic child) {
if (child is Navigator) {
return child;
}
// ignore: avoid_dynamic_calls
return _findNavigator(child.child);
}
final _routes = GoRouter(
routes: [
ShellRoute(
builder: (context, state, child) {
final navigator = _findNavigator(child);
return CustomNavigator(
key: navigator.key,
pages: navigator.pages,
onPopPage: navigator.onPopPage,
initialRoute: navigator.initialRoute,
onGenerateInitialRoutes: navigator.onGenerateInitialRoutes,
onGenerateRoute: navigator.onGenerateRoute,
onUnknownRoute: navigator.onUnknownRoute,
transitionDelegate: navigator.transitionDelegate,
reportsRouteUpdateToEngine: navigator.reportsRouteUpdateToEngine,
// >= 3.7.0
// clipBehavior: navigator.clipBehavior,
observers: navigator.observers,
requestFocus: navigator.requestFocus,
restorationScopeId: navigator.restorationScopeId,
);
},
routes: [
GoRoute(
path: '/',
builder: (context, state) => const RootPage(),
routes: [
GoRoute(
path: 'second',
builder: (context, state) => const SecondPage(),
)
],
),
],
)
],
);
@immutable
class AppRoot extends StatelessWidget {
const AppRoot({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
theme: ThemeData(
pageTransitionsTheme: const CustomPageTransitionsTheme(),
),
routerConfig: _routes,
);
}
}
@immutable
class RootPage extends StatelessWidget {
const RootPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(
'Pop gesture hack sample (GoRouter)',
),
),
body: ListView.builder(
itemBuilder: (context, index) {
return ListTile(
title: Text('$index'),
subtitle: const Text('Tap to push page'),
onTap: () => GoRouter.of(context).push('/second'),
);
},
),
);
}
}
@immutable
class SecondPage extends StatelessWidget {
const SecondPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(
'Pop gesture hack sample (GoRouter)',
),
),
body: const Center(
child: Text('Second page'),
),
);
}
} |
Reproduces on the latest versions of flutter. Steps to reproduce
updated sampleimport 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
void main() => runApp(const Root());
class Root extends StatelessWidget {
const Root({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(home: First());
}
}
class First extends StatefulWidget {
const First({super.key});
@override
_FirstState createState() => _FirstState();
}
class _FirstState extends State<First> {
var tappedTimes = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('First'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
child: const Text('Go To Second'),
onPressed: () {
setState(() {
tappedTimes++;
});
Navigator.of(context).push(
CupertinoPageRoute(
builder: (BuildContext context) {
return const Second();
},
),
);
},
),
InkWell(
child: Text('I\'m tapped $tappedTimes times'),
)
],
),
),
);
}
}
class Second extends StatelessWidget {
const Second({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Second'),
),
body: Container(),
);
}
} flutter doctor -v
|
The issue can be experienced in this UI package demo (disclosure: not affiliated at all). It has Flutter embedded so you can see it in action. If you push a new route and pop it with the iOS gesture of swiping on the left edge, the home route freezes for XYZms, even after the route animation is finished. The freeze can be seen in the hover effect button. Even if you hover the cursor quickly, the button does not have the hover effect until the freeze is over. If you pop the route with the appBar leading Icon then the freeze does not happen. Screen.Recording.2023-05-10.at.12.29.58.mov |
This issue also exists in |
Unfortunately my ShellRoute hack no longer works with go_router v13 😞 The Navigator widget is now built deep inside go_router's own _CustomNavigator widget and cannot be replaced. |
I found another way. I copied the entire code of
https://gist.github.com/toda-bps/f0d99dc5e87be019dbd71ebdfb7da435 By creating a However, this method is not ideal because it largely copies the code from the SDK, so it cannot keep up with Flutter updates, for example. |
cc @hangyujin since you fixed a similar issue in this area @toda-bps it looks like you found a fix already, do you want to submit a pr :) |
@toda-bps thank you for your awesome work! What is the current status? Why are they not accepting your PR? |
Sadly the problem still persists even 4 years after the report. |
Steps to Reproduce
(Code is modified from #41024)
https://imgur.com/a/4Ubzrj4
Here is a demo, in the first few times I clicked go back button on the top left and as you can see, I'm able to click 'Go To Second' immediately after navigating back to the first screen, however starting "I'm tapped 5 times", there is a significantly delay before 'Go To Second' button is clickable since swipe back is used.
Target Platform: iPhone / iPad
Target OS version/browser: iOS13.3
Devices: Reproduced in multiple devices
Logs
The text was updated successfully, but these errors were encountered: