ionic-framework
ionic-framework copied to clipboard
bug: ios, cannot disable Safari swipe to go back when running as PWA
Bug Report
Ionic version:
[ ] 4.x [x] 5.x
Current behavior:
Swipe gesture cannot be disabled on native iOS or Android devices. When swiping back, if using [swipeGesture]=true
the animation/back gesture will trigger in addition to browser navigation, resulting in what appears to be two simultaneous back navigation events, and sometimes, very inconsistent app state on completion.
Navigate to any URL on an iPad or Android device with browser swipe-back functionality. Navigate to another ion-page/route.
Drag finger from left edge of screen to right. Swipe gesture still triggers back navigation.
Expected behavior:
With [swipeGesture]=false
, The swipe gesture should be prevented and browser back navigation should not occur. With [swipeGesture]=true
only one of the browser back navigation or ionic built in back-transitions should occur, not both.
Steps to reproduce:
Generate a sample ionic application:
ionic start myApp sidemenu
ionic serve --external
set [swipeGesture]='false'
on ion-router-outlet in app.component.html
Navigate to the URL on an iPad or Android device with browser swipe-back functionality. Navigate to another page.
Drag finger from left edge of screen to right. Swipe gesture still triggers back navigation.
Related code:
Any brand new ionic generated app will demonstrate this issue.
insert short code snippets here
Other information:
Ionic info:
Ionic:
Ionic CLI : 6.11.1 (/usr/local/lib/node_modules/@ionic/cli)
Ionic Framework : @ionic/angular 5.3.5
@angular-devkit/build-angular : 0.1000.8
@angular-devkit/schematics : 10.0.8
@angular/cli : 10.0.8
@ionic/angular-toolkit : 2.3.3
Utility:
cordova-res : not installed
native-run : not installed
System:
NodeJS : v12.13.0 (/usr/local/lib/node_modules/node/bin/node)
npm : 6.14.5
OS : macOS Catalina
Thanks for the issue. Can you please provide a GitHub repo I can use to reproduce the issue? I can't reproduce the issue using the code snippets provided.
Thanks for the issue! This issue has been labeled as needs reproduction
. This label is added to issues that need a code reproduction.
Please provide a reproduction with the minimum amount of code required to reproduce the issue. Without a reliable code reproduction, it is unlikely we will be able to resolve the issue, leading to it being closed.
For a guide on how to create a good reproduction, see our Contributing Guide.
I've adjusted the styles to make the issue more apparent.
Repo: https://github.com/TopDecked/ionic-swipe-gesture-bug
I realized it also has to do with the menu:
Video of issue: https://photos.google.com/photo/AF1QipMiQEB4oBy8sfMnk4Q__uRjKddDWi3GNZ62rkc
Note, this happens both as PWA and as traditional webapp, though my recording shows the latter.
The swipe gesture with split pane is similarly odd, though I think this is possibly an issue that the Safari swipe to go back gesture needs to be blocked so that ionic can keep doing its thing:
swipeGesture=false
https://photos.app.goo.gl/dDYNfknq88xBMeoa8
Again, I've tested this in web and PWA on iOS 14.
The swipeGesture
property on ion-router-outlet
does not disable Safari/WKWebView's swipe to go back gesture, it is only intended to disable Ionic Framework's swipe to go back gesture.
We are not aware of a method for disabling this, but I can ask some WebKit folks if there is a way.
One thing you could do for that menu is use replaceUrl
which should disable the Safari swipe to go back gesture:
this.router.navigate([url], { replaceUrl: true });
Hmmm... interesting. I guess that makes some sense. Though... is more worrying than if this were an Ionic bug. Ionic bugs are more easily fixed... I'll be interested to hear what the Safari folks say.
Is there any way to disable the Ionic menu animation/function if Safari has started navigating back?
I did some digging and found a 'hack' that seems to disable edge swiping... sometimes. I turned it into a directive to make it easier to apply/test, but it seems like there's some timing issue as it only sometimes prevents the default Safari back action.
Essentially listen for the touchstart event and prevent default if it's near the edge.
It seems like this hack needs to be applied directly on the top-level element from which the event was fired to have any type of success. Again, I'm guessing there's some kind of timing issue on Safari here, unless I've incorrectly registered the non-passive listener somehow.
const EDGE_THRESHOLD = 10;
@Directive({
selector: '[stopEdgeSwipe]'
})
export class StopEdgeSwipeDirective {
constructor(private _el: ElementRef) {
this._el?.nativeElement?.addEventListener('touchstart', (e: Event) => {
const event = (<any>e);
if (event.pageX === undefined || event.pageX === null
|| (event.pageX > EDGE_THRESHOLD
&& event.pageX < window.innerWidth - EDGE_THRESHOLD)
) {
return true;
}
e.preventDefault(); // This only sometimes works, and the event never appears as if the default has been prevented after calling this. (Internal property is never updated.)
return false; // I don't know if this does anything or not.
}, {
passive: false
});
}
}
Good call with the suggestion. I agree that replaceUrl: true
would likely solve the problem since the browser history would constantly be replaced with the next state. This might be possible.
One snag, however, It does not appear to be possible to apply replaceUrl: true
globally, unless I'm missing something. So this would need to be added to all links and calls to Router.navigate
and NavController.navigate
. Am I missing a hook somewhere?
@liamdebeasi Also, I did some testing and I think replaceUrl
also needs to be accompanied by skipLocationChange: true
for that workaround to really work, which has the effect of... actually breaking all back buttons.
Otherwise, the browser still seems to record the history state (not sure why, I may have done something wrong.)
Could be useful, but may only be reasonable on PWA/native. Even so, you'd have to do some manual state saving of the current route to return users to where they left off.
I'll do some more testing.
Working on an interceptor for the built in angular Router
:
https://stackoverflow.com/questions/45514970/angular-2-override-router-navigate-method
Update. { replaceUrl: true }
seems to be all that is required. But I still don't think it's a good idea to do this outside of PWA/native. That said, I think those are the only places where this really matters.
Honestly, you guys said you were trying to figure out how to deal with the back button for PWAs. I think turning it off entirely might be the simplest solution (and could certainly be done as an opt-in setting,) if you have any desire to include this. Code below:
import { Location } from '@angular/common';
import { Compiler, Injector, NgModuleFactoryLoader, Type } from '@angular/core';
import { ChildrenOutletContexts, NavigationExtras, Router, Routes, UrlSerializer, UrlTree } from '@angular/router';
export class GlobalRouterDecorator extends Router {
private static _historyEnabled = true;
public static enableHistory(enabled: boolean = true) {
GlobalRouterDecorator._historyEnabled = !!enabled;
}
constructor(rootComponentType: Type<any>, urlSerializer: UrlSerializer,
rootContexts: ChildrenOutletContexts, location: Location, injector: Injector,
loader: NgModuleFactoryLoader, compiler: Compiler, config: Routes) {
super(rootComponentType, urlSerializer, rootContexts, location, injector, loader, compiler, config);
}
navigate(commands: any[], extras?: NavigationExtras): Promise<boolean> {
return super.navigate(commands, this.enhanceExtras(extras));
}
navigateByUrl(url: string | UrlTree, extras?: NavigationExtras) {
return super.navigateByUrl(url, this.enhanceExtras(extras));
}
private enhanceExtras(extras: NavigationExtras) {
return !GlobalRouterDecorator._historyEnabled ? {
...extras,
replaceUrl: true
} : extras;
}
}
Then in your app.module.xml
@NgModule({
declarations: [AppComponent],
imports: [...],
providers: [
Location, { provide: LocationStrategy, useClass: PathLocationStrategy },
{
provide: Router,
useFactory: routerFactory,
deps: [ApplicationRef, UrlSerializer, ChildrenOutletContexts, Location, Injector,
NgModuleFactoryLoader, Compiler, ROUTES]
]},
bootstrap: [AppComponent]
})
export class AppModule { }
function flatten<T>(arr: T[][]): T[] {
return Array.prototype.concat.apply([], arr);
}
export function routerFactory(rootComponentType: Type<any>, urlSerializer: UrlSerializer,
rootContexts: ChildrenOutletContexts, location: Location, injector: Injector,
loader: NgModuleFactoryLoader, compiler: Compiler, config: Route[][]): Router {
return new GlobalRouterDecorator(
rootComponentType,
urlSerializer,
rootContexts,
location,
injector,
loader,
compiler,
flatten(config)
);
}
Then in app.component.ts (or whatever Ionic setting):
if (this._plt.is('pwa')) {
GlobalRouterDecorator.enableHistory(false);
}
Update #2. This solution does not work for Chrome based browsers/PWAs. But things appear sliiiightly less broken there since they'll give you a chance to swipe twice to go back if the PWA is in fullscreen mode. It also does nothing to override the back button, if one is displayed in standalone PWA mode.
Spoke with some WebKit team members, and this is the gist of what they said:
- The main way they know of disabling this gesture is at the webview level via
-[WKWebView setAllowsBackForwardNavigationGestures:]
. - In terms of JavaScript, modifying the history stack could work to disable the gesture (I.e. using
history.replaceState
instead ofhistory.pushState
). This is similar to what I suggested with https://github.com/ionic-team/ionic-framework/issues/22299#issuecomment-707767466. - Gestures are an unresolved problem with the Web platform. They mainly spoke about issues around being able to detect and resolve conflict between gestures.
On a related note -- Are you able to swipe to go back on a Chromium-based PWA?
Thanks for looking into this @liamdebeasi - I appreciate the feedback! Looks like your initial answer was correct, and it appears to be working.
Gestures are an unresolved problem with the Web platform. They mainly spoke about issues around being able to detect and resolve conflict between gestures.
Tell me about it...
On a related note -- Are you able to swipe to go back on a Chromium-based PWA?
In short, yes. You can use swipe to go back on Chromium-based PWAs as of Android Q (Android 10). Only some devices seem to support this interaction, newer Pixel phones have it, as of the Pixel 3, I believe (100% certain about the pixel 4. I tested it myself and can send a video to demonstrate if it would be helpful). However, my Pixel 2 XL does not. Newer pixels have been removing the android control-bar in favor of gesture-based controls like iOS.
Do you guys have a stack of test devices over at Ionic? :) Can I borrow it ๐
Here are a few articles that describe the new edge-press/swipe navigation, and how Google has been approaching the gesture, since they acknowledge it interferes with menus and such.
https://www.androidpolice.com/2019/07/11/android-q-beta-5-gesture-nav-side-menu/ https://www.androidpolice.com/2019/03/21/chrome-gets-swipe-gestures-to-navigate-back-or-forward/
There do appear to be two conflated types of back-navigation however. I cannot find any docs for the above.
That said:
Overscroll navigation
is a separate feature defined in the WC3, and newer versions of Chrome do support this as well. It can be activated using multi-touch gestures (two fingers swipe right, etc). The appears to be controllable using the css overscroll-action
property:
https://wicg.github.io/user-gesture-nav/
And can also be managed by cancelling mousewheel
events, apparently:
https://stackoverflow.com/questions/23560528/how-to-prevent-overscroll-history-navigation-without-preventing-touchstart
The other issue (edge gestures) is much more insidious, and I have not been able to find any docs about controlling or disabling it.
I hope this is helpful.
On an unrelated note: I'd really love to show you this app sometime. It's been a massive undertaking and Ionic (w/Angular) has made it possible. We've had to work around a lot of issues since we're going full PWA. (Most Safari related. Keyboard heights & detection. Device rotation causing improper scaling. And the list goes on...)
In short, yes. You can use swipe to go back on Chromium-based PWAs as of Android Q (Android 10). Only some devices seem to support this interaction, newer Pixel phones have it, as of the Pixel 3, I believe (100% certain about the pixel 4. I tested it myself and can send a video to demonstrate if it would be helpful). However, my Pixel 2 XL does not. Newer pixels have been removing the android control-bar in favor of gesture-based controls like iOS.
Yeah if you wouldn't mind sending a screen recording, that would be great. I don't have a physical Android device running Android 10 at the moment.
Overscroll navigation is a separate feature defined in the WC3, and newer versions of Chrome do support this as well. It can be activated using multi-touch gestures (two fingers swipe right, etc). The appears to be controllable using the css overscroll-action property:
On that note, we've been discussing this more with the WebKit team and they suggested that this kind of gesture control would be good as a standard, as it can affect more than just iOS devices. We are currently looking at proposing an extension of touch-action
since that seems the closest in terms of semantics.
On an unrelated note: I'd really love to show you this app sometime. It's been a massive undertaking and Ionic (w/Angular) has made it possible. We've had to work around a lot of issues since we're going full PWA. (Most Safari related. Keyboard heights & detection. Device rotation causing improper scaling. And the list goes on...)
Would love to see it! Feel free to shoot me an email liam [at] ionic.io.
Yeah if you wouldn't mind sending a screen recording, that would be great. I don't have a physical Android device running Android 10 at the moment.
Here are two recordings that show the basic issue in slightly different scenarios (same basic issue). Let me know if you have any issues accessing the Google Photos/Drive link. It should be public:
https://photos.google.com/share/AF1QipOV5cR5_RV1hQfOsrbFEf5vMuyvDf6f4xoUg7GiucBRqDu7Wt1CX3GkmRf2QVWl3w/photo/AF1QipNwOlDsdVJ6uAr4m0ItctLsFlAOeIUUjh50Rws?key=WTZNbDI2aUZRWk9IN1pMRnVCUHRMWmdBU3Jpd3lB
If you notice, this PWA is set to "fullscreen" in metadata.json, however, that's not necessary to reproduce the issue as this also occurs on the web, in "native" Chrome.
On that note, we've been discussing this more with the WebKit team and they suggested that this kind of gesture control would be good as a standard, as it can affect more than just iOS devices. We are currently looking at proposing an extension of touch-action since that seems the closest in terms of semantics.
I'm glad to hear this is being discussed. It obviously can't come soon enough :) This is one rare scenario when Safari was easier to tame than Chrome is. Have you had any discussions with the Chrome team about this? I'd be interested to hear their thoughts.
Would love to see it! Feel free to shoot me an email liam [at] ionic.io.
Ill shoot you an email :)
@liamdebeasi Just a quick heads up on the tags for this issue. Not sure "needs reproduction" is correct as the videos display the issue using a sample app. Not a big deal just don't want this to get lost in the mix. More later, things got busy this week.
Here are two recordings that show the basic issue in slightly different scenarios (same basic issue). Let me know if you have any issues accessing the Google Photos/Drive link. It should be public:
Thanks! This is really helpful.
I'm glad to hear this is being discussed. It obviously can't come soon enough :) This is one rare scenario when Safari was easier to tame than Chrome is. Have you had any discussions with the Chrome team about this? I'd be interested to hear their thoughts.
We have only had talks with the WebKit team. Once we put up the proposal I imagine we could ask some Chromium folks to take a look.
@lincolnthree regarding swipe gesture in safari - as you mentioned setting swipeGesture to false is not sufficient, but if you disable animations (set animated = false in ion-nav or ion-router-outlet) it will work as expected: safari native back gesture will work properly. Also you can disable swipeGesture in menu. In my case I check what platform is running the app and either enable/disable animations and swipe gesture.
@MarkChrisLevy The issue here is we want neither. The fact that both occur is just bad icing on a terrible cake of confusion :)
That's why such a complex workaround is currently necessary.
Hi everyone. Just developing right now an Ionic 5 + Angular 11 APP which has to be deployed in Android + iOS (Native) + Web APP + PWA.
Facing the same issue, just in case it's useful for anyone, I kind of solved this "double" gesture binding (browser + ionic) with the following, as Mark & Lincoln suggested above.
...
import { Platform } from '@ionic/angular';
import { IonRouterOutlet } from '@ionic/angular';
...
constructor(
...
private routerOutlet: IonRouterOutlet,
private platform: Platform
...
) { }
...
ngOnInit() {
if ( this.platform.is('mobileweb') || this.platform.is('pwa') )
{
// In these cases, you already get the browser swipe-back gesture, so disable ionic's.
this.routerOutlet.swipeGesture = false;
}
}
Of course, this is only a workaround, but since it's something that for me (as maybe for many people) is kind of a basic and expected behavior to be covered out of the box, I'd share it.
Hope the guys at the team got this to work soon!
Best,
Hi everyone,
After discussing with the WebKit team, I created a proposal for a way to disable the swipe back gesture using touch-action
in CSS: https://github.com/w3c/csswg-drafts/issues/6161
Any feedback on this proposal is appreciated!
@liamdebeasi Awesome! I'll check it out. Thanks for continuing to pursue this!
๋ค ๊ทธ๋ผ ์ผ๋จ ์คํ์ ํ๊ณ ๋ค๋ฅธ์ด์ ํ๋ฒ ๋ณด์๊ณ ์ก์๋ณผ๋์?
Hi everyone,
I got a few questions about this issue this week and wanted to post a quick summary here:
What is the issue?
When running an Ionic app in Safari or as a PWA on iOS the platform's swipe to go back gesture (STGB) is running at the same time as Ionic's swipe to go back gesture (STGB), leading to unexpected flickering/rendering on the page.
Why is this happening?
Safari/WebKit provides its own built in STGB that conflicts with Ionic Framework's STGB as they get triggered via the same touch interaction.
For developers using WKWebView, there is an API to disable the built in STGB that WebKit provides, but no such API exists for developers using Safari or WebKit in a PWA.
Is this a bug in Ionic Framework?
No. This is not a bug in WebKit either. Both functionalities are triggering when they were designed to be triggered. The problem is that they conflict and interfere with the functionality of Ionic Framework's STGB.
Is there a solution?
The long term solution is for browser vendors to expose an API that allow web developers to control this behavior. I created a proposal for the W3C Pointer Events group: https://github.com/w3c/pointerevents/issues/358
There is no great short term solution. If this is a blocker for your app, I recommend disabling Ionic Framework's STGB in the application config using the swipeBackEnabled
functionality. While this will not disable Safari's built in STGB, it will at least prevent the two gestures from interfering.
Ionic Angular config: https://ionicframework.com/docs/angular/config#config-options Ionic React config: https://ionicframework.com/docs/react/config#config-options Ionic Vue config: https://ionicframework.com/docs/vue/config#config-options
For hybrid apps of the iOS version, is it possible to disable the back gesture through settings? This can solve at least part of the problem https://developer.apple.com/documentation/webkit/wkwebview/1414995-allowsbackforwardnavigationgestu
Apps running as a PWA do not have access to that API. Cordova/Capacitor apps use that API so this issue does not impact them.
@liamdebeasi Thank you very much for your follow-up My program runs on Capacitor But this problem also exists on my iphone12, and the gesture back conflict will occasionally appear. And after fast switching (browse\back) several times, two pages will appear on the interface at the same time (each one occupies a part). In this case, the smart solution is solved by restarting the app.
If you are running into issues where the webview is allowing a swipe back then I recommend filing an issue on the Capacitor repo: https://github.com/ionic-team/capacitor
Ionic Framework does not have control over the webview.