Repository navigation
Description
iOS edge swipe-back never completes when go_router is combined with cupertino_ui's CupertinoPageTransitionsBuilder
Steps to reproduce
- Use the repro below ("Config Unfortunately, Sky has stopped #4 — broken").
- Run on an iOS simulator or device.
- Tap an item to push a detail route via
go_router. - Swipe from the left screen edge, all the way to the right (or a fast full-width flick).
Expected results
The page follows the finger and completes the pop on release, same as before migrating off flutter/cupertino.dart.
Actual results
The drag visually tracks the finger, but releasing never completes the pop — the page always snaps back to the detail screen, even with a full-width, fast flick. This reproduces 100% of the time (tested 4/4 attempts, both with a slow full-width drag and a fast flick, and with the built-in swipe gesture helper).
This is not a general app freeze: the back button and all other taps keep working normally immediately after a failed swipe attempt. Only the edge-swipe gesture's pop-completion is affected.
Isolation: this is an interaction between go_router and cupertino_ui, not either alone
We bisected across 5 configurations (all other things equal — same routes, same theme, same gesture):
| # | Flutter | Cupertino transitions builder from | go_router | Swipe-back completes? |
|---|---|---|---|---|
| 1 | 3.44.2 | flutter/cupertino.dart (built-in) |
17.3.0 | ✅ works |
| 2 | 3.44.2 | flutter/cupertino.dart (built-in) |
17.5.0 | ✅ works |
| 3 | 3.47.0 | flutter/cupertino.dart (built-in, still bundled pre-Fall-deprecation) |
17.5.0 | ✅ works |
| 4 | 3.47.0 | package:cupertino_ui 1.0.0 |
17.5.0 | ❌ broken |
| 5 | 3.47.0 | package:cupertino_ui 1.0.0 |
(no go_router — plain Navigator.push/MaterialPageRoute) |
✅ works |
Neither cupertino_ui alone (#5) nor go_router alone (#2, #3) reproduces the bug. Only the combination of package:cupertino_ui's CupertinoPageTransitionsBuilder and go_router's route/page building (#4) breaks gesture completion. Configs #4 and #3 differ by exactly two import lines — see the two full files below.
Code sample — Config #4 (broken)
pubspec.yaml:
dependencies:
flutter:
sdk: flutter
go_router: 17.5.0
material_ui: ^1.0.0
cupertino_ui: ^1.0.0main.dart (config #4 — broken: cupertino_ui + go_router)
import 'package:cupertino_ui/cupertino_ui.dart';
import 'package:material_ui/material_ui.dart';
import 'package:go_router/go_router.dart';
final _router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
routes: [
GoRoute(
path: 'detail/:index',
builder: (context, state) =>
DetailScreen(index: state.pathParameters['index']!),
),
],
),
],
);
void main() => runApp(const ReproApp());
class ReproApp extends StatelessWidget {
const ReproApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: _router,
theme: ThemeData(
useMaterial3: false,
pageTransitionsTheme: const PageTransitionsTheme(
builders: {
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.android: CupertinoPageTransitionsBuilder(),
},
),
),
);
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('NEW - Flutter 3.47 / material_ui')),
body: ListView.builder(
itemCount: 10,
itemBuilder: (context, i) => ListTile(
title: Text('Item $i'),
onTap: () => context.push('/detail/$i'),
),
),
);
}
}
class DetailScreen extends StatelessWidget {
const DetailScreen({required this.index, super.key});
final String index;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Detail $index')),
body: Container(
color: Colors.amber.shade100,
alignment: Alignment.center,
child: Text(
'Detail screen $index\nSwipe from left edge to go back',
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 20),
),
),
);
}
}main.dart (config #3 — works: differs ONLY by the two import lines below — replace cupertino_ui/material_ui imports with flutter/cupertino.dart/flutter/material.dart, everything else byte-identical, same go_router 17.5.0)
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
final _router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
routes: [
GoRoute(
path: 'detail/:index',
builder: (context, state) =>
DetailScreen(index: state.pathParameters['index']!),
),
],
),
],
);
void main() => runApp(const ReproApp());
class ReproApp extends StatelessWidget {
const ReproApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: _router,
theme: ThemeData(
useMaterial3: false,
pageTransitionsTheme: const PageTransitionsTheme(
builders: {
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.android: CupertinoPageTransitionsBuilder(),
},
),
),
);
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('CROSS2 - Flutter 3.47 / flutter.cupertino')),
body: ListView.builder(
itemCount: 10,
itemBuilder: (context, i) => ListTile(
title: Text('Item $i'),
onTap: () => context.push('/detail/$i'),
),
),
);
}
}
class DetailScreen extends StatelessWidget {
const DetailScreen({required this.index, super.key});
final String index;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Detail $index')),
body: Container(
color: Colors.amber.shade100,
alignment: Alignment.center,
child: Text(
'Detail screen $index\nSwipe from left edge to go back',
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 20),
),
),
);
}
}main.dart (config #5 — works: cupertino_ui, but plain Navigator.push/MaterialPageRoute, no go_router at all)
import 'package:cupertino_ui/cupertino_ui.dart';
import 'package:material_ui/material_ui.dart';
void main() => runApp(const ReproApp());
class ReproApp extends StatelessWidget {
const ReproApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
useMaterial3: false,
pageTransitionsTheme: const PageTransitionsTheme(
builders: {TargetPlatform.iOS: CupertinoPageTransitionsBuilder()},
),
),
home: const HomeScreen(),
);
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: Center(
child: FilledButton(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const DetailScreen()),
),
child: const Text('Push detail'),
),
),
);
}
}
class DetailScreen extends StatelessWidget {
const DetailScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Detail')),
body: const Center(
child: Text(
'Swipe from the left edge to go back.\n\n'
'Expected: page follows the finger and pops on release '
'(same as flutter/cupertino.dart pre-3.44).\n'
'Actual: the drag tracks visually but releasing never '
'completes the pop - the page snaps back every time, '
'even with a full-width fast flick.',
textAlign: TextAlign.center,
),
),
);
}
}Code-level lead: likely site of the bug
We compared cupertino_ui 1.0.0's lib/src/route.dart against the copy still bundled in the Flutter 3.47.0 SDK (packages/flutter/lib/src/cupertino/route.dart, used by config #3 above). The gesture-completion logic — _CupertinoBackGestureController.dragEnd — is byte-for-byte identical in both:
void dragEnd(double velocity) {
const Curve animationCurve = Curves.fastEaseInToSlowEaseOut;
final bool isCurrent = getIsCurrent();
final bool animateForward;
if (!isCurrent) {
// If the page has already been navigated away from, then the animation
// direction depends on whether or not it's still in the navigation stack,
// regardless of velocity or drag position. ...
// See https://github.com/flutter/flutter/issues/141268.
animateForward = getIsActive();
} else if (velocity.abs() >= _kMinFlingVelocity) {
animateForward = velocity <= 0;
} else {
animateForward = controller.value > 0.5;
}
...This is significant: it means the bug is not a code change inside cupertino_ui's gesture/transition logic. The if (!isCurrent) branch is the only path that ignores velocity and drag position entirely, and is the only mechanism we can find that would produce exactly what we observe — the page always snaps back, regardless of how far or how fast the swipe was (4/4 attempts, both slow full-width drags and fast flicks). getIsCurrent/getIsActive read route.isCurrent/route.isActive, which are plain Route<T> getters from package:flutter/widgets.dart (routes.dart/navigator.dart) — untouched by the Material/Cupertino decoupling, and identical in every config we tested.
Our working theory: something in how go_router builds/manages its Page/Route objects (bookkeeping tied to its declarative rebuild cycle, possibly related to the route metadata feature added in go_router 17.4.0/17.5.0) causes route.isCurrent to read false during a live edge-swipe gesture even though the route is visually on top — tripping the !isCurrent branch and forcing animateForward regardless of the gesture. This would explain why the same decision code produces different real-world outcomes only when go_router is present.
We were not able to confirm this at runtime. We instrumented a local copy of cupertino_ui's route.dart with print() calls in dragEnd, _handleDragStart/Update/End/Cancel, and _handlePointerDown (logging isCurrent, isActive, velocity, controller.value), pointed the broken repro at it via dependency_overrides: {cupertino_ui: {path: ...}}, and confirmed via strings on the compiled kernel_blob.bin that the instrumented code was present and running (navigation still worked normally). However, none of flutter run's attached log output, xcrun simctl spawn log stream, nor xcrun simctl launch --console-pty captured any of that output in our sandboxed environment — all three produced zero matching lines despite the instrumented code executing. This looks like a tooling limitation on our end (no working console passthrough for the iOS Simulator here), not a finding about the bug itself. Whoever picks this up on a normal local dev machine should be able to get real values out of the same instrumentation in a few minutes and confirm or refute the isCurrent/isActive hypothesis directly.
Additional context
This was found while migrating an app from Flutter 3.44 to 3.47 (dart fix --code=migrate_design_widgets, flutter/material.dart+flutter/cupertino.dart → material_ui+cupertino_ui). Checked both packages' changelogs on pub.dev — neither documents a behavior change here (cupertino_ui's changelog says it's a straight copy of the SDK code as of the freeze). go_router's changelog between 17.3.0 and 17.5.0 also documents nothing related (route metadata support, an onExit/ShellRoute fix). This suggests the regression is in how cupertino_ui's CupertinoPageTransitionsBuilder/route interacts with the Page/Route object go_router builds internally, not a documented behavior change in either package.
Flutter doctor -v (config #4)
[✓] Flutter (Channel stable, 3.47.0, on macOS 26.5.2 25F84 darwin-arm64, locale fr-FR) [991ms]
• Flutter version 3.47.0 on channel stable
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision 4cf2416426 (3 days ago), 2026-08-11 11:53:49 -0700
• Engine revision 5f77625673
• Dart version 3.13.0
• DevTools version 2.60.0
Tested on iOS Simulator (iPhone 17, iOS latest).
Activity
[-]iOS edge swipe-back never completes when go_router is combined with cupertino_ui's CupertinoPageTransitionsBuilder[/-][+][go_router] iOS edge swipe-back never completes with cupertino_ui page transitions[/+]mbcorona commentedon Aug 15, 2026
HI @TheoGermain , thanks for the detailed report and bisection. Routing to the framework team.
jonmountjoy commentedon Aug 17, 2026
I just found that I couldn't swipe back after pushing a page, when I could before. This may be related:
It turns out GoRouter does this:
context.findAncestorWidgetOfExactType<MaterialApp>()andcontext.findAncestorWidgetOfExactType<CupertinoApp>That looks for the "old"
MaterialApp/CupertinoAppnot the newmaterial_ui/cupertion_uiversions, and so they won't produce the right answer (depending on the platform).justinmc commentedon Aug 18, 2026
Good catch @jonmountjoy! I guess we just need to migrate go_router to the new packages.
[go_router] migrates to material_ui and cupertino_ui (#12504)
github-actions commentedon Sep 2, 2026
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of
flutter doctor -vand a minimal reproduction of the issue.