27

How can I build a webpage which is able to monitor when the page gets the focus, especially when Safari is in the background and the user switches Safari back to the foreground.

The code below does not fire the event when switching to Safari on an iPhone

<html>
  <head>
    <script type="text/javascript">
      window.onfocus = function() { alert("onfocus"); };
    </script>
  </head>
  <body>

    Main text

  </body>
</html>

According http://www.quirksmode.org/dom/events/index.html : Safari iPhone does not fire the event when the window gains the focus.

So my question is still: how to detect by using Javascript on a web page within Safari for iPhone that the window receives the focus?

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
Axel Derks
  • 343
  • 1
  • 3
  • 6
  • can you explain why you would need to know it? maybe there is another way? for example using timers. – Magnus Jan 11 '11 at 10:07
  • 1
    Many people move the webapp to the background. When they open it again (e.g. a few days later), it must update the screen to the latest state. – Axel Derks Jan 11 '11 at 19:39

7 Answers7

16

Depending on what you need to support, you need a variety of different techniques to detect when a page becomes visible. Variations occur due to browser vendor, browser version, OS, running within WebView/UIWebView/WKWebView, etc.

You can view which events are occurring by using this page. I have found that to detect when the page "wakes up" on all combinations I needed to register all of the following events:

  • window visibilitychange event
  • window focus event
  • window pageshow event
  • starting a timer and seeing if the timer took much longer than it should have (timers are put to sleep by iOS when hibernated). An App using UIWebView doesn't fire the visibilityChange event even on iOS9 (WKWebView is OK).

I used to use webkitRequestAnimationFrame as well, but I removed that because it could cause jank (AFAIK the rendering engine does a blocking call to the main thread for it).

Things to try:

  • Go to another tab
  • Lock screen, wait, unlock
  • Bring another app to focus
  • Minimise browser

You can see which events are happening:

  • in real time by looking at the console log (attach debugger).
  • in real time on a device by using http://output.jsbin.com/rinece#http://localhost:80/ and see the log get requested as Ajax calls (use a proxy, or run a small server on the address after the # and log the body to console).
  • look at the on screen log, and pay close attention to the time logged for each entry to see if the entry was logged e.g. visibilitychange hide event might not occur when page is actually hidden (if app is hibernated), but instead is queued and occurs when page is reshown!!!

iOS: beware if using a timer to detect if an iOS UIWebView has gone to sleep, you need to measure the difference using new Date.getNow() and not performance.now(). That is because performance.now() stops counting time when the page is put to sleep also iOS was slow to implement performance.now()... (Aside: you may be able to measure the amount of time the page was asleep for by detecting the discrepency of differences for new Date.getNow() and performance.now(). Look for the != on the test page).

If you are using UIWebView then there are two techniques that work (You must use UIWebViewif you support an iOS7 App). WKWebView has the visibilitychange event so workarounds are not required.

==Technique 1.

When the applicationWillEnterForeground event occurs in the app, call UIWebView stringByEvaluatingJavaScriptFromString to call your JavaScript pageAwakened().

Benefits: clean, accurate.

Downside: needs Objective-C code. Called function needs to be accessable from global scope.

==Technique 2.

Use webkitRequestAnimationFrame and detect a time lag.

Benefits: JavaScript only. Works for mobile Safari on iOS7.

Downside: ugly risk of jank and using webkitRequestAnimationFrame is a severe hack.

// iOS specific workaround to detect if Mobile App comes back to focus. UIWebView and old iOS don't fire any of: window.onvisibilitychange, window.onfocus, window.onpageshow
function iosWakeDetect() {
    function requestAnimationFrameCallback() {
        webkitRequestAnimationFrame(function() {
            // Can't use timestamp from webkitRequestAnimationFrame callback, because timestamp is not incremented while app is in background. Instead use UTC time. Also can't use performance.now() for same reason.
            var thisTime = (new Date).getTime();
            if (lastTime && (thisTime - lastTime) > 60000) {    // one minute
                // Very important not to hold up browser within webkitRequestAnimationFrame() or reference any DOM - zero timeout so shoved into event queue
                setTimeout(pageAwakened, 0);
            }
            lastTime = thisTime;
            requestAnimationFrameCallback();
        });
    }
    var lastTime;
    if (/^iPhone|^iPad|^iPod/.test(navigator.platform) && !window.indexedDB && window.webkitRequestAnimationFrame) {    // indexedDB sniff: it is missing in UIWebView
        requestAnimationFrameCallback();
    }
}

function pageAwakened() {
    // add code here to remove duplicate events. Check !document.hidden if supported
};

window.addEventListener('focus', pageAwakened);
window.addEventListener('pageshow', pageAwakened);
window.addEventListener('visibilitychange', function() {
    !document.hidden && pageAwakened();
});
robocat
  • 5,293
  • 48
  • 65
15

I believe timers (setInterval()) are suspended when the app enters the background. You could do something like:

var lastFired = new Date().getTime();
setInterval(function() {
    now = new Date().getTime();
    if(now - lastFired > 5000) {//if it's been more than 5 seconds
        alert("onfocus");
    }
    lastFired = now;
}, 500);

You may need to adjust those time intervals to suite your needs.

But, most likely, if it has been long enough to need a refresh (a few days) safari will probably reload the page because it is out of memory.

David Beck
  • 10,099
  • 5
  • 51
  • 88
  • It even refresh if we leave the safari without any activity in foreground. So if some one is reading the news, page get reloaded. What we want is that it should reload only once we come back from background to foreground. – Varun Jul 11 '12 at 07:37
  • Web pages in safari are the first thing to get unloaded if another foreground app needs more resources. If another app needs more memory and Safari chose to unload your webpage, you will not get any notification of the unload and the webpage will be reloaded when you come back. – Eric Falsken Apr 29 '13 at 18:17
  • There's a problem with this approach though. When the user is scrolling the page, setInterval is suspended as well. So if the user scrolls the page for more than 5 seconds, the alert will be fired too - which is probably not desired. – Tony Lâmpada Dec 02 '13 at 19:44
  • This works if, for example, you leave the webapp fullscreen and press the 'lock' button, then unlock the screen. However it doesn't work if you switch apps by pressing the home button as this results in the entire web app reloading from scratch. I guess in this scenario that doesn't matter as you will have stuff firing off onload anyway. But as @EricFalsken mentioned you can't get any notification of an unload. – Adam Marshall Jan 22 '14 at 12:06
9

I wrote a little test page to see what events are being sent to the window on iOS.

The page is "Apple web app capable", so you can save it to the home screen and test it in standalone mode.

Here's the page: Test of Window Events

The code:

// determine if this is a touch-capable device
const isTouchDevice = ('ontouchstart' in window) ||
  (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0);
console.log(`isTouchDevice: ${isTouchDevice ? 'TRUE' : 'FALSE'} `);

const button = document.getElementById('btnClear');
const divEvents = document.getElementById('divEvents');
const olEvents = document.getElementById('olEvents');
const divBottom = document.getElementById('divBottom');

// handle "clear history" button click
button.addEventListener('click', function() {
  if (isTouchDevice) {
    // simulate click on button using `focus` and `blur`
    button.focus();
    setTimeout(() => button.blur(), 500);
  }
  olEvents.innerHTML = '';
});

const eventNames = [
  'load',
  'focus',
  'blur',
  'change',
  'close',
  'error',
  'haschange',
  'message',
  'offline',
  'online',
  'pagehide',
  'pageshow',
  'popstate',
  'resize',
  'submit',
  'unload',
  'beforeunload'
];
eventNames.forEach(function(eventName) {
  window.addEventListener(eventName, function(evt) {
    const now = new Date();
    const timeStr = now.getHours().toString().padStart(2, '0') + ':' +
      now.getMinutes().toString().padStart(2, '0') + ':' +
      now.getSeconds().toString().padStart(2, '0') + '.' +
      now.getMilliseconds();
    let li = document.createElement('li');
    li.innerHTML = timeStr + ' - ' + `<code>${evt.type}</code>`;
    olEvents.appendChild(li);

    // scroll to bottom
    // window.scrollTo(0, divBottom.offsetTop);
    const bottomOffset = divBottom.offsetTop;
    divEvents.scrollTop = bottomOffset - 10;
  });
});
#divEvents {
  border: 1px solid rgba(0, 0, 0, 0.5);
  height: 400px;
  max-width: 60rem;
  padding: 1rem 0;
  overflow-y: auto;
}

#olEvents {
  font-size: 87.5%;
}

#divBottom {
  height: 0px;
}

code {
  font-size: 100%;
}


/* handle the sticky hover problem on touch devices */

@media (hover:none) {
  /* set button hover style to match non-hover */
  .btn-outline-primary:hover {
    color: #007bff;
    background-color: transparent;
    background-image: none;
    border-color: #007bff;
  }
  /* set button focus style to match hover */
  .btn-outline-primary:focus {
    color: #fff;
    background-color: #007bff;
    border-color: #007bff;
  }
}
<head>
  <!-- Required meta tags -->
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, maximum-scale=1, minimum-scale=1, shrink-to-fit=no, user-scalable=no">

  <!-- apple web app meta tags -->
  <meta name="apple-mobile-web-app-title" content="WinEvents">
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black">

  <title>Test of Window Events</title>

  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootsdark@latest/dist/bootsdark.min.css">

</head>

<body class="d-flex flex-column h-100">
  <header>
    <!-- Fixed navbar -->
    <nav class="navbar navbar-expand-md navbar-dark bg-dark">
      <a class="navbar-brand" href="https://terrymorse.com">Terry Morse
      Software</a>
    </nav>
  </header>

  <main role="main" class="flex-shrink-0 m-4">
    <h1>Test of Window Events</h1>

    <p>Displays all of the events (except for
      <code>scroll</code>) sent to <code>document.window</code>.</p>

    <p>
      <button id="btnClear" class="btn btn-sm btn-outline-primary"
      >Clear History</button>
    </p>

    <h4>Events Caught:</h4>
    <div id="divEvents">
      <ol id="olEvents" class="text-monospace"></ol>
      <div id="divBottom"></div>
    </div>
  </main>

</body>
terrymorse
  • 6,771
  • 1
  • 21
  • 27
  • On i(Pad)OS 15.1, it looks like there is a 'resize' event (actually, 3 of them) when the browser comes back into the foreground. WHat it will be in iPadOS 15.2 or 16 is anyone's guess... – rakensi Dec 08 '21 at 16:15
  • 1
    This example is iOS Safari debugging gold. – suchislife Mar 09 '22 at 20:28
8

The Page Visibility API would probably offer a solution to this Problem. I guess this API has not been implemented in Mobile Safari yet, at least I haven't found any documentation for an iOS implementation. However, an Implementation has been commited to the Webkit Trunk, so there is a chance that it will be supported by future Versions of Mobile Safari.

liquidki
  • 1,234
  • 16
  • 16
Simon
  • 3,509
  • 18
  • 21
  • 1
    This is a nice idea, but it is now be implemented in iOS7 (http://caniuse.com/#feat=pagevisibility) but it doesn't solve the problem by itself. If you switch tabs in Safari it works, but if you switch applications (either through single press or double press of the home button) it doesn't. – Adam Marshall Jan 22 '14 at 11:40
  • Did you find a solution to this problem? Seems the PageVisibility API still doesn't work with the home button... – andybarnes Jan 31 '14 at 10:22
  • The page visibility API works for me in safari and chrome. Check out https://developer.mozilla.org/en-US/docs/Web/Guide/User_experience/Using_the_Page_Visibility_API – Burgi Feb 17 '14 at 16:51
  • 1
    For Apps on iOS, my [testing](https://jsbin.com/rinece/) shows that the visibilityChange event does not occur in a UIWebView regardless of what iOS version you are using. It does occur in WKWebView. – robocat Feb 22 '16 at 02:46
  • This is currently working on mobile safari iOS 14.2. I get events when I tap the home button to background the app, then when I tap the application icon to restore, and also when switching between apps. – liquidki Dec 16 '20 at 08:44
1

The focus and blur events on the window are good to detect if the browser is going to or back from background. This worked for me on iOS8 Safari:

window.addEventListener("focus", function(evt){
    console.log('show');
}, false);
window.addEventListener("blur", function(evt){
    console.log('hide');
}, false);
Martin Cup
  • 2,399
  • 1
  • 21
  • 32
  • I tested 'focus' event detection on iPhone XS and iPad Pro, iOS 12.1. It is working, although two focus events are detected each time Safari is activated. – terrymorse Dec 02 '18 at 21:45
-1

Since the problem is with mobile safari and it supports popstate event, you can use this event to detect when the user is back

Christian Quirós
  • 494
  • 1
  • 5
  • 9
-2

Use the pageshow and pagehide events.

<script type="text/javascript">
        window.addEventListener("pageshow", function(evt){
            alert('show');
        }, false);
        window.addEventListener("pagehide", function(evt){
            alert('hide');
        }, false);
</script>

https://developer.apple.com/library/mac/documentation/AppleApplications/Reference/SafariWebContent/HandlingEvents/HandlingEvents.html

lembas
  • 377
  • 1
  • 7
  • 21