Skip to content

Notifications in iOS Home Screen apps

Published

Notifications in an iOS Home Screen web app combine a user-granted permission, a push subscription surface, a service worker, and notification events. Use these field notes to reproduce the behaviour measured on iOS 18.6.2, investigate a permission or click path, or compare a later build without turning one device result into a general support claim.

The recorded run used one iPhone 16 Pro on iOS 18.6.2 (22G100) on 2026-10-20. Prepare an HTTPS app with a manifest set to display: standalone, a service worker, a permission button, an IndexedDB event log, and a way to create a local notification. Add the app to the Home Screen before testing. WebKit documents Web Push for Home Screen web apps from iOS and iPadOS 16.4 and requires notification permission to be requested from direct user interaction (Web Push for Web Apps on iOS and iPadOS, webkit.org).

The test did not send a real remote push. It measured permission, subscription objects, locally displayed notifications, notification taps, and worker state. The device study and public data package preserve the run.

WebKit documents Web Push for Home Screen web apps from iOS and iPadOS 16.4. The recorded behaviour below is limited to one installed Home Screen app on iOS 18.6.2; it does not establish notification support in Safari tabs, other releases, or remote-push delivery, which this run did not test.

Open the app from its Home Screen icon and test the interfaces that the flow will use. Keep a fallback for a Safari tab or another context where they are absent:

function notificationSurface() {
const installed = matchMedia('(display-mode: standalone)').matches
|| navigator.standalone === true;
const supported = 'Notification' in window
&& 'serviceWorker' in navigator
&& 'PushManager' in window;
if (!installed || !supported) {
return { enabled: false, action: 'Open the Home Screen app' };
}
return { enabled: true, action: 'Show permission control' };
}

The fallback is part of the flow. On the recorded build, Notification and the PushManager constructor were absent in a Safari tab and present in the installed app. iOS 18.4 also introduced window.pushManager for Declarative Web Push; the run found that property in both contexts, so testing that property alone did not identify the installed notification surface.

Start with an app that has not asked before. Put Notification.requestPermission() directly inside a button handler, record performance.now() before the call, and record the settled result afterward. Do not trigger the request at page load. WebKit’s documentation states that the request must come from direct user interaction.

permissionButton.addEventListener('click', async () => {
const started = performance.now();
permissionButton.disabled = true;
const permission = await Notification.requestPermission();
const elapsed = Math.round(performance.now() - started);
eventLog.textContent = JSON.stringify({ permission, elapsed });
if (permission !== 'granted') {
permissionButton.disabled = false;
fallbackPanel.hidden = false;
return;
}
subscribeButton.hidden = false;
});

In the archived run, the system prompt appeared after the tap, and the promise remained pending until the operator answered. One recorded grant settled after 2068 ms. That number describes the recorded interaction; it is not a timeout value for application code (Notification permission prompt).

If the result is denied or default, keep the in-app fallback visible and do not immediately ask again. If the promise never settles during a test, preserve a screen recording, the app’s prior permission state, and the system build before changing the code.

From iOS and iPadOS 18.4, WebKit exposes Declarative Web Push through window.pushManager. WebKit says that a root-scoped service worker shares the window object’s subscription; the 18.6.2 test used workers scoped to subpaths, so that sharing condition was not met (Meet Declarative Web Push, webkit.org).

After permission is granted, read both surfaces without creating another subscription:

const registration = await navigator.serviceWorker.ready;
const workerSubscription = await registration.pushManager.getSubscription();
const windowSubscription = window.pushManager
? await window.pushManager.getSubscription()
: null;
console.table({
worker: workerSubscription?.endpoint ? 'present' : 'missing',
window: windowSubscription?.endpoint ? 'present' : 'missing',
permission: Notification.permission,
});

The recorded Home Screen app had a service-worker subscription whose endpoint host was web.push.apple.com; window.pushManager.getSubscription() returned null. One permission grant made both managers’ permission-state checks return granted. The result is consistent with WebKit’s root-scope condition because the tested worker used non-root demo or Device Lab scopes, not / (see the subscription-scope record). A root-scoped worker was not tested, and the test did not deliver a remote push.

Register the event with addEventListener and assign self.onnotificationclick as separate probes. Write each invocation to IndexedDB before focusing a client so the next page load can display the result.

sw.js
self.addEventListener('notificationclick', (event) => {
event.notification.close();
event.waitUntil(recordClick('listener').then(() => focusClient()));
});
self.onnotificationclick = (event) => {
event.notification.close();
event.waitUntil(recordClick('property').then(() => focusClient()));
};
async function focusClient() {
const clients = await self.clients.matchAll({ type: 'window' });
return clients[0]?.focus();
}

Tap a displayed notification from its banner and from Notification Centre, then repeat after relaunching and after swiping the app away. In the study, both handlers ran for each of six recorded taps, and clients.matchAll() found the open window so focus() succeeded. The clients.openWindow() path was not reached (Notification clicks in Home Screen apps).

This device result and BCD describe different contexts. BCD records api.ServiceWorkerGlobalScope.notificationclick_event as unsupported for Safari on iOS. Issue #22959 explains that its collector checks for the handler property in a service worker; the 18.6.2 study found 'onnotificationclick' in self false when the worker was controlled by a Safari tab and true when controlled by the Home Screen app (worker handler exposure; BCD issue #22959, github.com).

If a tap does not reach the handler, first confirm that the notification belongs to the installed app, the worker containing the handler is active, and the event log survives a page relaunch. Record whether an app window was already alive. WebKit bug reports include failures when no page is alive, while this run reached an existing client and did not exercise openWindow().

Do not infer that a missing log proves the event did not fire until the write path has also been checked. The study found an observer error during the run and retained corrected notebook entries. Save the notification source, handler registration method, app lifecycle state, worker script URL, system build, and raw IndexedDB log.