Question

iOS has a convention that double-tapping on the top bar (i.e., where the current time is displayed) scrolls the app to the top state. For example, double-tapping the top bar in Safari brings you to the top of the current web page, and double-tapping the top in Facebook/Twitter brings you to the top of the feed. It's a very useful navigation shortcut.

Let's call it a TopTap for purposes of this question.

I'm wondering how I can detect TopTaps in a JavaScript app in mobile Safari -- that is, NOT in an iOS app, but in a web page that happens to be viewed in mobile Safari.

In my particular case, I can't rely on the built-in mobile Safari TopTap behavior because my document consists of a single <canvas> element that implements its own scrollable interface. I want to be able to detect a TopTap so that I can scroll that <canvas> to its top state.

I've experimented with adding an onscroll event handler, but there's no distinguishing information in that event that would let me isolate the TopTaps. Also, I can't use touch events (touchstart, etc.), because a TopTap happens in the browser/OS chrome, outside the scope of the web page.

Any ideas?

Was it helpful?

Solution

As it turns out double tapping the native status bar will trigger a scroll event on document.body which you can in fact listen for. Trick is, as you mentioned, how can you determine if it's from a double tap or not?

In order to detect it, the body has to be scrolled down to begin with. And setting the scroll position causes a scroll event.

While it's a bunch of hacks, I've been able to get this working:

Code: https://github.com/HenrikJoreteg/holy-grail-ios-layout

Demo: http://ios-layout.surge.sh

Basically, you don't really use the <body> as a container. You set it as position: absolute and full height/width. Then you have some kind of container element that you set to position: fixed that you use as your actual container for your content.

Doing this lets you programmatically scroll the body without affecting anything visual on the page at all.

Now you're set up to listen for scroll events on the body and ideally the user can't actually cause a scroll on the body except via double tapping the status bar.

Unfortunately you have to do all sorts of silly things to make this work.

  1. Put something taller than 100% in the body so the body can actually has something to scroll.
  2. Programmatically set the scroll position to 1 to start and after each status bar double tap.
  3. Set a debounced scroll handler on the body that only fires if it knows the event wasn't caused by setting the position to 1.
  4. As it turns out, iOS also likes to break thing when you rotate the phone, etc.
  5. Use the following CSS to make the contents of your actual content containers scrollable with momentum and rubber-banding:

overflow-y: scroll; /* has to be scroll, not auto */ -webkit-overflow-scrolling: touch;

Anyway, it's something :)

More info in the github repo, but anyway, this really does seem to work quite well for iOS 8 (haven't tested other versions of iOS).

OTHER TIPS

If your target is recent iOS, you could put an element over your canvas that is position: fixed to the top of the viewport and use that to detect double-taps.

EDIT: I was thinking something like the below, but as Adrian points out he needs it to happen when the native browser chrome is double-tapped as well.

<!doctype html>
<html>
    <head>
        <style type="text/css">
        canvas {
            height: 1000px;
            width: 320px;
        }
        #top-tap {
            height: 16px;
            left: 0;
            position: fixed;
            right: 0;
            top: 0;
        }
        </style>
    </head>
    <body>
        <canvas></canvas>
        <a id="top-tap"></a>
        <script type="text/javascript">
        function secondTap() {
            window.scrollTo(0,0)
        }
        document.querySelector('#top-tap').addEventListener('touchend', function (e) {
            var self = this
            this.addEventListener('touchend', secondTap)
            setTimeout(function () {
                self.removeEventListner('touchend', secondTap)
            }, 100)
        })
        </script>
    </body>
</html>

Is it possible to create a hidden native html element, the same height as the content in your canvas. Then map the scroll position on the native element to the same position within the canvas. Could also hide the canvas scroll widget.

So as far as the users concerned they only see the native scroll bar... but all the scroll events map to the canvas - including the status bar tap.

May not work if you have other HTML content on the page but might if there's only the canvas visible.

Edit: Here's a very crude prototype http://jsbin.com/cisizopi/3

I'm simulating a canvas and it's contents with divs

After doing a little research, I came across this post: http://prud.github.io/ios-overflow-scroll-to-top/. I'm not sure if this will do what you are looking for. Also, I'm fairly certain that you cannot intercept the status bar touch with JS in the browser for IOS.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top