Where We Are Now With PWAs On iOS

Where We Are Now With PWAs On iOS
Photo by Andrew Johnson / Unsplash

Modern Web Weekly #23

Apple's decision to disable PWAs on iOS 17.4 has been keeping developers busy and caused a lot of confusion. Many developers are still not fully up to date on the consequences and there is a lot of contradictory information.

I will share an article I wrote in an attempt to clear up the confusion here, but first...

Coming soon to a browser near you

@starting-style

A lot of new features have gained support in the major browsers (Chrome, Edge, Safari, and Firefox) starting with two new features in Safari Tech Preview 189.

This version now supports @starting-style, a new CSS feature that enables animating the opening and closing of modals and popovers. This is not possible with regular CSS since modals and popovers since CSS transition only work for elements that are rendered and this is not the case for popovers and modals since these elements have display: none when they're not visible.

The @starting-style grouping rule enables us to define styles for where the transition should start from for elements that are not being rendered when the transition starts.

Here's the CSS we should add if we want a <dialog> element to fade in and slide down when it's opened:

@starting-style {
  dialog[open] {
    opacity: 0;
    translate: 0 -50px;
  }
}

Note that we define the styles with the selector dialog[open] and not dialog. This is because when the transition starts, the dialog already has the open attribute.

To make the animation work we also need to transition the display and overlay properties which is possible with transition-property: allow-discrete. This means that when the animation to show the <dialog> is started, display is changed from none to block immediately so the transitioning of the opacity property is visible. When the <dialog> is hidden, display keeps the value block until the end of the transition so again the transitioning of the opacity property is visible and at the end of the transition, its value is changed to none.

In addition to display, we also need to transition the overlay property. A modal dialog is displayed in the so-called "top-layer" of the document which is a layer that is displayed above all other elements on the page. This layer ensures that a dialog or popover is always displayed on top of everything else. An element that is in the top-layer has its overlay property set to auto and when it's not in the top-layer the value of overlay is none.

overlay can only be set by the browser to add or remove an element from this top-layer so you cannot set it with CSS but you can transition/animate it. By animating the overlay property you ensure that the element stays in the top-layer while the transition or animation is running so it won't be obscured or clipped by any other elements on the page.

/* Styles for exit state, when the dialog is closed */
dialog {
  opacity: 0;
  translate: 0 -50px;
  transition-property: opacity, translate, overlay, display;
  transition-duration: .3s;
  transition-behavior: allow-discrete;
}

/* Styles when the dialog is open */
dialog[open] {
  opacity: 1;
  translate: 0 0;
}

/* Styles of the dialog before it's opened, where to transition from */
@starting-style {
  dialog[open],
  :popover-open {
    opacity: 0;
    translate: 0 -50px;
  }
}

Safari Tech Preview 189 doesn't support overlay yet so there will be no exit transition but the entry transition works.

Check it out for yourself in this Codepen (you may need to view it in the browser version of this email):

Safari Tech Preview 189 also ships with support for the Cookie Store API although you need to enable this feature flag (Develop > Feature Flags).

The Cookie Store API provides an asynchronous interface for getting and setting cookies, so instead of doing this:

// setting a cookie with name "cookie1"
document.cookie = `cookie1=cookie1-value`;

//with expiry date and domain
document.cookie = `cookie1=cookie1-value; expires=Tue Feb 27 2024 20:07:56 GMT+0100; domain=example.com`;

// getting a cookie with name "cookie1"
const namedCookie = document.cookie.split(';')
      .find((cookie) => cookie.trim().startsWith(`cookie1=`));

const cookieValue = namedCookie? namedCookie.split('=')[1] : null;

You can now do this:

// setting a cookie with name "cookie1"
await cookieStore.set("cookie1", "cookie1-value");

//with expiry date and domain
await cookieStore.set({
  name: "cookie1",
  value: "cookie1-value",
  expires: Date.now(),
  domain: "example.com",
})

// getting a cookie with name "cookie1"
const cookie = await cookieStore.get("cookie1");

There are two other great features of the Cookie Store API and the first one is that you can set an event handler on it that is invoked whenever a cookie is changed or deleted:

cookieStore.addEventListener("change", (event) => {
  console.log(event);
});

The event passed to the handler is of type CookieChangeEvent and has the properties changed and deleted that contain an array of cookies that were changed and deleted respectively.

The second feature is that cookies are now also available to Service Workers through self.cookieStore. Getting and setting cookies is done in the same as in the above examples and Service Workers can listen for change events by subscribing to these events through the CookieStoreManager that is accessible through self.registration.cookies.

To receive cookie change events, a Service Worker calls the subscribe method of CookieStoreManager with an array of subscriptions:

const subscriptions = [{ name: "cookie1", url: `/path1` }];

await self.registration.cookies.subscribe(subscriptions);

self.addEventListener("cookiechange", (event) => {
  console.log(event);
});

A subscription is an object with the name of the cookie and the url scope of the cookie. In the above example, the Service Worker would receive a change event when a cookie with the name "cookie1" is changed or deleted and the "url" scope of the cookie is "/path1".

To stop listening for change events, the Service Worker calls the unsubscribe method of CookieStoreManager:

const subscriptions = [{ name: "cookie1", url: `/path1` }];

await self.registration.cookies.unsubscribe(subscriptions);

A good use case for cookies in Service Workers is that the Service Worker can store an authentication token in a cookie when a user logs in and then add it as an Authentication header to any network calls that need authentication.

Declarative Shadow DOM in Firefox 123

Firefox 123 now ships with support for Declarative Shadow DOM so server-side rendering of Web Components now has cross-browser support! 🎉

PWAs On iOS: Where We Are Now

PWAs are still broken in the beta versions of iOS 17.4 and it looks like this will still be the case in the final version. This is not good news for the future of PWAs and it leaves me frustrated so I had to let off some steam on X.

🚨
If you care about the future of web apps on the open web, please consider signing this open letter to Tim Cook, CEO of Apple: https://letter.open-web-advocacy.org

There is still a lot of confusion around what the consequences of Apple's actions are so I wrote the following article on Medium in an attempt to clear up the confusion:

Read the original article here

The havoc Apple has wreaked upon Progressive Web Apps (PWA) by sabotaging them completely on iOS 17.4 has left developers worldwide frustrated and confused. Not only because it’s an incomprehensible turnaround after their recent improvements in PWA support, but also because the situation Apple left us with is even more puzzling.

In this article, I will attempt to clear up the confusion.

Only in the EU

After the word leaked out that Apple broke PWAs on iOS 17.4 beta 1, confused developers started sharing screen recordings of iPhone simulators running the offending version in which PWAs still installed and ran fine.

Alas, this was not the situation on real iPhones running in the EU.

Apple restricted the change to the EU to comply with DMA regulations and did so through geofencing. It uses the SIM card to determine the user’s location and since software-based simulators don’t have SIM cards, the problem won’t show up there. If you were to run a new iPhone in the EU without a SIM card, PWAs will still work fine and reportedly also when you take out the SIM of an older iPhone and leave it on Flight Mode for 24 hours.

Anyhow, you need a real iPhone with a SIM card located in an EU country to experience the issue.

Installed PWAs are now just bookmarks

Another source of confusion is that you can still “install” a web app on iOS 17.4, meaning: an icon will be placed on the user’s Home Screen but the buck stops here.

When you click the icon, the app will not be opened as a standalone PWA without browser chrome anymore, but just like a regular website. In fact, this is how any website that is not a PWA is opened when you add it to the Home Screen. In other words, a web app that is “installed” is not really installed but just added to the Home Screen as a bookmark.

Currently, this is true for any browser that you can install a PWA with on iOS since all browsers on iOS are Webkit browsers under the hood.

What makes a PWA display like a full screen standalone app is the "display": "standalone" property in the manifest.json file. A website that doesn’t have a manifest.json file and that’s added to the Home Screen is opened in a browser like any other website when the icon is clicked. If it does have a manifest.json file with "display": "standalone" this is simply ignored on iOS 17.4 and the PWA is reduced to a bookmark.

Besides the fact that this completely breaks the UI, it also disables crucial functionality.

No more push notifications

Apple only added support for push notifications a year ago on iOS 16.4. Besides the fact that their implementation is suboptimal, push notifications are only supported for installed PWAs.

That is: installed PWAs running as standalone apps.

Now that “installed” web apps on iOS are no longer standalone apps but just bookmarks of websites, push notifications and badging are no longer supported.

Sorry kid.

No more persistent storage

Persistent storage was only recently added to Safari on iOS but since that is also only available for installed PWAs running in standalone mode, you can say hasta la vista to that feature as well.

PWAs used to be excluded from Apple’s eviction policy that dictates that data stored by any web app that hasn’t been interacted with for 7 days will be evicted, but not anymore for the website bookmarks that PWAs have become on iOS 17.4.

This renders PWAs completely useless and is probably the biggest nail in the coffin that Apple carefully prepared for them.

A security issue?

Apple claims that PWAs installed through non-Webkit browsers are a security risk because iOS provides support for PWAs by directly building on the Webkit security architecture. This security integration takes care of showing permission prompts when the app tries to access the camera for example and provides isolated storage.

Since non-Webkit browsers don’t have this security integration on iOS, installing PWAs through them would not be safe and would even result in PWAs being installed without the user’s permission.

Of course this is nonsense because Apple can build such an integration (like it has had for years on MacOS) but it doesn’t want to. To prevent being forced to build such an integration, it throws up a technical smokescreen and tries to get away with it (but it looks like it won’t).

Separating permissions and data by origin is one of the most important core functionalities of any browser, so there are no security issues other than the ones Apple made up to protect their App Store monopoly.

Apple is trying to kill the only viable alternative to their App Store that’s free from unreasonably high fees and arbitrary admission criteria.

Don’t let them fool you into thinking they’re not.

đź”—
Got an interesting link for Modern Web Weekly?Send me a DM on Twitter to let me know!