如何在iOS Web应用程序中检测从后台切换回Safari?

28

我该如何构建一个网页,能够监视该页面何时获得焦点,尤其是当Safari处于后台且用户将Safari切换回前台时。

下面的代码在iPhone上切换到Safari时不会触发事件。

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

    Main text

  </body>
</html>
根据http://www.quirksmode.org/dom/events/index.html的说法:Safari iPhone在窗口获得焦点时不触发事件。
因此,我的问题仍然是:如何通过在Safari iPhone上使用JavaScript检测窗口接收焦点?

你能解释一下为什么需要知道它吗?也许有另一种方法?例如使用计时器。 - Magnus
1
许多人将Web应用程序移至后台。当他们再次打开它(例如几天后),它必须将屏幕更新到最新状态。 - Axel Derks
7个回答

17

根据需要支持的内容,需要使用不同的技术来检测页面何时变为可见。由于浏览器供应商、浏览器版本、运行在WebView/UIWebView/WKWebView中等原因,可能会出现差异。

您可以使用此页面查看正在发生的事件。我发现,在所有组合上检测页面“唤醒”的时间需要注册以下所有事件:

  • window visibilitychange 事件
  • window focus 事件
  • window pageshow 事件
  • 启动计时器并查看计时器是否比应该花费的时间更长(当处于休眠状态时,iOS会使计时器进入睡眠状态)。使用UIWebView的应用程序甚至在iOS9上也不会触发visibilityChange事件(WKWebView是可以的)。

我曾经使用过webkitRequestAnimationFrame,但我将其删除了,因为它可能会导致卡顿(据我所知,渲染引擎对主线程进行了阻塞调用)。

试一试:

  • 转到另一个选项卡
  • 锁定屏幕,等待,解锁
  • 将另一个应用程序置于焦点
  • 最小化浏览器

您可以看到正在发生的事件:

  • 通过查看控制台日志(附加调试器)实时查看。
  • 在设备上实时使用http://output.jsbin.com/rinece#http://localhost:80/并查看日志作为Ajax调用请求(使用代理或在#后的地址上运行一个小型服务器,并将主体记录到控制台)。
  • 查看屏幕上的日志,并密切关注记录每个条目所需的时间,以确定是否记录了该条目,例如当页面实际隐藏时,visibilitychange hide事件可能不会发生(如果应用程序处于休眠状态),而是排队等待页面重新显示时发生!

iOS:如果使用计时器检测iOS UIWebView是否已进入睡眠状态,请注意,您需要使用new Date.getNow()来测量差异,而不是performance.now()。因为performance.now()停止计算时间时,页面也被放置到休眠状态中,此外,iOS实现performance.now()时速度较慢……(另外:您可以通过检测new Date.getNow()performance.now()之间的差异来测量页面的休眠时间。在测试页面上查找!)。

如果使用UIWebView,则有两种有效技术(如果要支持iOS7应用程序,必须使用UIWebView)。WKWebView具有visibilitychange事件,因此无需使用解决方法。

==技巧1。

当应用程序在前台运行时,调用UIWebView的stringByEvaluatingJavaScriptFromString方法来调用你的JavaScript页面被唤醒的函数pageAwakened()。
优点:干净,精确。
缺点:需要Objective-C代码。调用的函数需要从全局作用域访问。
技术2:
使用webkitRequestAnimationFrame检测时间延迟。
优点:只需JavaScript。适用于iOS7上的移动Safari。
缺点:丑陋的风险和使用webkitRequestAnimationFrame是一个严重的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();
});

15

我相信当应用程序进入后台时,计时器(setInterval())会被暂停。你可以这样做:

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);

你可能需要调整这些时间间隔以适应你的需求。

但是,最有可能的情况是,如果需要刷新(几天),Safari可能会重新加载页面,因为内存已经不足。


即使我们离开Safari而没有任何前台活动,它甚至也会刷新。因此,如果有人正在阅读新闻,页面将重新加载。我们想要的是,它应该只在我们从后台返回前台时重新加载。 - Varun
如果另一个前台应用程序需要更多资源,Safari 中的网页将是首先被卸载的内容。如果另一个应用程序需要更多内存,并且 Safari 选择卸载您的网页,则您将不会收到任何卸载通知,并且当您回来时,该网页将重新加载。 - Eric Falsken
然而,这种方法存在问题。当用户滚动页面时,setInterval 也会被暂停。因此,如果用户滚动页面超过5秒钟,警报也将被触发 - 这可能不是期望的结果。 - Tony Lâmpada
如果您将Web应用程序全屏并按下“锁定”按钮,然后解锁屏幕,则此方法有效。但是,如果您通过按主页按钮切换应用程序,则此方法无效,因为这会导致整个Web应用程序从头开始重新加载。我猜在这种情况下,无论如何都会有一些东西在onload时触发。但正如@EricFalsken所提到的,您无法获得任何卸载通知。 - Adam Marshall

10

我写了一个小测试页面来查看 iOS 中发送到窗口的事件。

该页面具有“Apple Web App 可能性”,因此您可以将其保存到主屏幕并在独立模式下进行测试。

这是页面:Window 事件测试

代码:

// 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>


在iPadOS 15.1上,当浏览器重新回到前台时,似乎会出现一个“调整大小”事件(实际上有3个)。至于iPadOS 15.2或16会是什么样子,谁也说不准... - rakensi
1
这个例子就是iOS Safari调试的黄金。 - suchislife

8

页面可见性API 可能会提供解决此问题的方案。我猜测这个API尚未在移动版Safari中实现,至少我没有找到任何有关iOS版本的文档。然而,在Webkit分支中已经提交了一个实现,因此未来的Mobile Safari版本可能会支持它。


1
这是一个不错的想法,但它现在已经在iOS7中实现了(http://caniuse.com/#feat=pagevisibility),但它本身并不能解决问题。如果你在Safari中切换标签,它可以工作,但如果你切换应用程序(通过单击或双击主页按钮),它就无法工作了。 - Adam Marshall
你找到解决这个问题的方法了吗?似乎PageVisibility API仍然无法与主页按钮一起使用... - andybarnes
页面可见性 API 对于 Safari 和 Chrome 浏览器来说对我有效。请查看 https://developer.mozilla.org/zh-CN/docs/Web/Guide/User_experience/Using_the_Page_Visibility_API。 - Burgi
1
对于iOS上的应用程序,我的测试显示无论您使用哪个iOS版本,visibilityChange事件都不会在UIWebView中发生。但是它会在WKWebView中发生。 - robocat
目前在移动版Safari iOS 14.2上运行正常。当我点击主页按钮将应用程序置于后台时,我会收到事件通知,然后当我点击应用程序图标进行恢复时,以及在应用程序之间切换时也会收到事件通知。 - liquidki

1
窗口的焦点和失焦事件非常适合用于检测浏览器是否进入或返回后台。这在iOS8 Safari上对我有效:
window.addEventListener("focus", function(evt){
    console.log('show');
}, false);
window.addEventListener("blur", function(evt){
    console.log('hide');
}, false);

我在 iPhone XS 和 iPad Pro,iOS 12.1 上测试了“focus”事件检测。虽然每次激活 Safari 时会检测到两个焦点事件,但它是有效的。 - terrymorse

-1

由于问题出现在移动版 Safari 上,而且它支持 popstate 事件,因此您可以使用此事件来检测用户何时返回。


-2

当Safari处于后台或重新回到前台时,pagehide/pageshow事件未被触发。 - Tom Roggero

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接