iOS swipe-away lifetime and storage
Published
Swiping an iOS Home Screen web app away does not by itself prove that its document has ended or that its storage has returned to Safari’s store. In one iPhone 16 Pro test on iOS 18.6.2, the same document was alive when a notification was tapped about six seconds after the swipe; separate tests found isolated localStorage and IndexedDB stores, plus a one-time cookie copy when an app was added.
Use this procedure when a notification tap unexpectedly focuses an old page, or when a newly installed app does not see data written in Safari. You need an installed HTTPS web app, Web Push permission for the lifetime path, a service worker with a notificationclick handler, and Mac Safari Web Inspector. The observed timings and storage values come from the 2026-10-20 iPhone 16 Pro, iOS 18.6.2 (22G100) run (study data package, openpwa.net).
Where it is supported
Section titled “Where it is supported”The measured lifetime and storage results apply to Safari and an installed Home Screen web app on an iPhone 16 Pro running iOS 18.6.2. The Service Worker specification defines clients.matchAll() as returning existing clients and clients.openWindow() as creating a new top-level browsing context (Service Workers, matchAll(), w3.org; Service Workers, openWindow(), w3.org). It does not set a lifetime for an iOS Home Screen app after a swipe.
WebKit says Home Screen web app data is isolated from Safari. Its Safari 17.2 release post adds that cookies are copied when the user adds a web app, no other local storage is copied, and later website data is not shared (Tracking Prevention in WebKit, webkit.org; WebKit Features in Safari 17.2, webkit.org). The 18.6.2 device test reproduced that documented model for the tested first-party cookie, localStorage, and IndexedDB values.
The lifetime case covered one notification tapped about six seconds after one swipe. It did not test longer intervals, memory pressure, Low Power Mode, a restart, iPadOS, or another iPhone. The storage case did not cover CacheStorage, OPFS, HttpOnly cookies, session cookies, or partitioned cookies.
Examples
Section titled “Examples”Run the identity, notification, and storage checks as separate observations so one result does not stand in for another.
Detect the notification path
Section titled “Detect the notification path”Check the installed display context and the APIs required by the notification test before scheduling a push. If the page is open in Safari or any required API is absent, skip the push path and use the document-identity and storage checks below as the fallback.
const standalone = matchMedia('(display-mode: standalone)').matches || navigator.standalone === true;const notificationPath = 'serviceWorker' in navigator && 'Notification' in window && 'PushManager' in window;
if (!standalone || !notificationPath) { console.warn('Push path unavailable; run the identity and storage checks instead.');} else { console.log('Installed context and notification APIs detected.');}The fallback still lets you compare document identity across a manual relaunch and confirm which storage context is active. It does not reproduce the archived notification-click path.
Give each document an identity
Section titled “Give each document an identity”Capture performance.timeOrigin before leaving the app. It identifies the start of the current document’s time coordinate; compare both reads after the notification returns the app to the foreground, but interpret them with the navigation type and history length rather than requiring exact equality. The archived N38 and N40 records contain those three signals; they did not capture an in-memory token.
const identity = { timeOrigin: performance.timeOrigin, navigationType: performance.getEntriesByType('navigation')[0]?.type, historyLength: history.length};
localStorage.setItem('document-identity', JSON.stringify(identity));console.log(identity);The N38 relaunch printed a timeOrigin of 08:28:29.978Z, navigation type navigate, and history.length 1. After the later N40 swipe and notification tap, the read printed 08:28:30.036Z; the study records the 58 ms difference as within the rounding of performance.timeOrigin across reads, with history.length 1 in both, so it identified the same document (page lifetime after a swipe-away).
Use an in-memory value as a second signal because a new document can read localStorage left by an old one. The fallback branch below treats a missing in-memory token as a new document and creates one.
if (globalThis.__documentToken) { console.log({ outcome: 'same-js-realm', token: globalThis.__documentToken });} else { globalThis.__documentToken = crypto.randomUUID(); console.log({ outcome: 'new-js-realm', token: globalThis.__documentToken });}Run it before the swipe and again after returning. The same token supports continued JavaScript-realm identity; a new token shows that this signal did not survive. Preserve both time-origin reads as supporting evidence, allowing the archived run’s recorded 58 ms rounding difference. Process ID is unsuitable because the recorded web-app process hosted several pages.
Handle a notification tap without assuming a dead page
Section titled “Handle a notification tap without assuming a dead page”Search for window clients first. Focus a matching client when one exists, and call openWindow() only when no match exists. Both outcomes must be logged so a test can distinguish them.
self.addEventListener('notificationclick', (event) => { event.notification.close();
event.waitUntil((async () => { const windows = await clients.matchAll({ type: 'window', includeUncontrolled: true });
const existing = windows.find((client) => new URL(client.url).origin === self.location.origin );
if (existing) { try { await existing.focus(); console.log({ windowsFound: windows.length, outcome: 'focused existing window' }); } catch (error) { console.error({ windowsFound: windows.length, outcome: 'focus failed', error }); throw error; } } else { try { const opened = await clients.openWindow('/'); if (opened) { console.log({ windowsFound: 0, outcome: 'opened new window' }); } else { console.error({ windowsFound: 0, outcome: 'openWindow returned null' }); } } catch (error) { console.error({ windowsFound: 0, outcome: 'openWindow failed', error }); throw error; } } })());});In the archived case, the notification was created at 08:43:03.022 UTC and tapped at 08:43:09. The handler output recorded { windowsFound: 1, outcome: "focused existing window" } at 08:43:09.236Z. The page’s property handler then recorded the tap at 08:43:09.245Z. Swiping the card away did not reach the no-client branch in that interval.
Mark each storage context
Section titled “Mark each storage context”Write a marker in Safari before adding the app. Use separate keys for localStorage, IndexedDB, and a cookie so the first app launch reveals which values arrived. This compact console example covers localStorage and the cookie; use the lab harness for its IndexedDB record.
const context = matchMedia('(display-mode: standalone)').matches ? 'APP' : 'TAB';const marker = `${context}-${new Date().toISOString()}`;
localStorage.setItem('storage-marker', marker);document.cookie = `storage_marker=${encodeURIComponent(marker)}; Path=/; Max-Age=31536000; SameSite=Lax; Secure`;
console.log({ localStorage: localStorage.getItem('storage-marker'), cookie: document.cookie});In the recorded sequence, Safari wrote TAB-2026-10-20T07:13:46Z before the apps were added. PWA Lab’s first read showed null in localStorage and IndexedDB, while its cookie contained that Safari marker. After the app wrote APP-2026-10-20T07:37:40Z, Safari still returned its earlier TAB-... values. A second app of the same site also kept a separate store (Safari storage and cookies in iOS Home Screen apps).
Check the display context before interpreting a missing marker. If matchMedia('(display-mode: standalone)').matches is false and navigator.standalone is not true, the read comes from Safari. If the app sees no localStorage or IndexedDB marker, initialize its own state or ask the user to sign in; do not treat Safari’s local data as an installation transfer. A copied cookie can carry the tested login-state mechanism described by WebKit, but later writes remain separate under the documented model.
Run the swipe test
Section titled “Run the swipe test”Record the identity, schedule a notification for five seconds later, go Home, open the App Switcher, and swipe the app away. Tap the notification when it arrives, then capture the handler log and identity again. The recorded 18.6.2 case found one window; its N38 and N40 time-origin reads differed by 58 ms within the study’s recorded rounding, while both records showed navigation type navigate and history.length 1. The archived run did not record an in-memory token. When reproducing the test, run the token check above as an additional signal. Do not require the two archived time-origin reads to be exactly equal.
For a no-page test, first establish that Web Inspector no longer lists the page, then trigger the notification from a source that does not require the page to stay alive. Record the elapsed time and any restart or memory-pressure condition. If the handler does not run, preserve the notification, console, service worker, and OS-build evidence; do not classify the result as an openWindow() rejection unless the handler log shows that branch was entered.
Keep storage testing separate from document-lifetime testing. A surviving document retains its active JavaScript state, while a relaunched document can still open the installed app’s isolated persistent stores. Neither outcome shows that Safari and the installed app share a live store.