the-editor 6 years ago

What is wrong with PWA support on iOS?

On 30th of March long awaited iOS 11.3 update was released, with support for basic PWA features on iPhones and iPads - service workers and app manifest files.

As it is great to finally have a support for those, user experience of Progressive Web Applications on iOS is still not perfect.


That means that many of released PWAs are still having serious issues on Apple devices when providing nearly native experience on Android at the same time. In the frontend community comments of disappointment were already raised, and the list of issues and bugs is long. Don't lose your hope, though! Below you will find a few tips on how to fix these issues and make your PWA as close to a regular mobile app as you can.


In the latest iOS update, Apple added support for service workers and app manifest. Now you can leverage caching with service workers and make your PWA work without internet connection. Let's remember - this is a basic requirement within the PWA definition. And unfortunately there are a few drawbacks in Apple's implementation.


SO WHAT IS WRONG?

Service workers support is very limited compared to Android. You can only persist app data and cache its files (no background tasks). There is also a 50MB and “few weeks” limit for storage.


iOS 11.3 also introduced support for the manifest file. But our tests showed that it is far from perfect. Icons are not working perfectly (or at all) and there’s no support for launch screen - you’ll get only a blank white screen when the app is loading. The app is reloaded each time it goes from the background, and there's no support for push notifications and many others functionalities, which are essential for a mobile app. To sum up - overall UX is quite bad.


IS PROVIDING NATIVE EXPERIENCE IN PWA ON iOS NOT POSSIBLE?

Thankfully, there's a lot you can do to improve your app's look and provide better UX. With a few simple tricks, you can build an app that in a lot of cases will be indistinguishable from a native one.


I believe that PWAs are the future of inclusive, performative and intuitive web so that’s why I would like to share some tips to overcome iOS limitations in terms of PWA.


#PROTIP 1: Make app icon great again (on every device)

We've encountered that iOS does not use icons from manifest and that makes a shortcut to your app on the home screen look really bad. There is a simple solution to overcome this issue - just add apple-touch-icon meta tag with proper image. But avoid icons with transparency - those will not work.

Code
<!-- place this in a head section --> <link rel="apple-touch-icon" href="touch-icon-iphone.png"> <link rel="apple-touch-icon" sizes="152x152" href="touch-icon-ipad.png"> <link rel="apple-touch-icon" sizes="180x180" href="touch-icon-iphone-retina.png"> <link rel="apple-touch-icon" sizes="167x167" href="touch-icon-ipad-retina.png">

And now your app will look perfect from the beginning!


#PROTIP 2: Fix the launch screen

Launch screen is displayed before the app is fully loaded and ready to use. Unfortunately iOS doesn't support launch screen generated from manifest as it is on Android - instead, it shows white, blank screen That's definitely not the experience we would like to serve to our users.


Thankfully we've found the solution which is described in apple developer's page. Apple supports custom meta tags to specify pre-generated splash screen - apple-touch-startup-image. So you just have to generate splash images in proper sizes which you can find listed below:


DevicePortrait SizeLandscape Size
12.9'' iPad Pro2048px x 2732 px2732px x 2048px
10.5'' iPad Pro1668px x 2224px2224px x 1668px
9.7'' iPad1536px x 2048px2048px x 1536px
7.9'' iPad mini 41536px x 2048px2048px x 1536px
iPhone X1125px x 2436px2436px x 1125px
iPhone 8 Plus1242px x 2208px2208px x 1242px
iPhone 8750px x 1334px1334px x 750px
iPhone 7 Plus1242px x 2208px2208px x 1242px
iPhone 7750px x 1334px1334px x 750px
iPhone 6s Plus1242px x 2208px2208px x 1242px
iPhone 6s750px x 1334px1334px x 750px
iPhone SE640px x 1136px1136px x 640px


When you have your stunning launch screens ready, the only thing left to do is to link them in the head section.

Like this:

Code
<!-- place this in a head section --> <meta name="apple-mobile-web-app-capable" content="yes" /> <link href="/apple_splash_2048.png" sizes="2048x2732" rel="apple-touch-startup-image" /> <link href="/apple_splash_1668.png" sizes="1668x2224" rel="apple-touch-startup-image" /> <link href="/apple_splash_1536.png" sizes="1536x2048" rel="apple-touch-startup-image" /> <link href="/apple_splash_1125.png" sizes="1125x2436" rel="apple-touch-startup-image" /> <link href="/apple_splash_1242.png" sizes="1242x2208" rel="apple-touch-startup-image" /> <link href="/apple_splash_750.png" sizes="750x1334" rel="apple-touch-startup-image" /> <link href="/apple_splash_640.png" sizes="640x1136" rel="apple-touch-startup-image" />

Now that ugly white screen on launch is gone.


#PROTIP 3: Create “Add to home screen” popup yourself!

On Android, there is a native popup which encourages the user to add an app to a home screen and informs him that our page is a PWA. Unfortunately, on iPhone there's no such thing, so our visitor is not even aware of our app capabilities. Moreover on iOS it requires as much as 3 taps to add the app to the home screen. But don't worry, we have a fix for that! We can add a custom popup which will indicate that our app can be added to home screen.


You are free to design that popup as you wish, our example is shown below. The hard part is to display it only in safari and not in standalone mode (when the app is already added to home screen). You can check if your app is in standalone mode with window.navigator.standalone.


Take a look at this snippet:

Code
// Detects if device is on iOS const isIos = () =&gt; { const userAgent = window.navigator.userAgent.toLowerCase(); return /iphone|ipad|ipod/.test( userAgent ); } // Detects if device is in standalone mode const isInStandaloneMode = () =&gt; ('standalone' in window.navigator) &amp;&amp; (window.navigator.standalone); // Checks if should display install popup notification: if (isIos() &amp;&amp; !isInStandaloneMode()) { this.setState({ showInstallMessage: true }); }

Just bear in mind that on iPad the share button is located at the top of a screen, next to the address bar, so you should change out popup's location accordingly.


#PROTIP 4: Take care of navigation

To make sure your app will be usable in standalone mode you have to check if you have implemented your navigation correctly. On Apple devices there's no back button, so you have to make sure that user is able to go back from any screen using navigation built-in to your app.

You can do that by displaying back button or by adding additional menubar if on iOS.


#PROTIP 5: Prepare for offline

To provide the true native experience you should be prepared for weak or no connection at all. In some simple cases, persisting store data will be enough, but this is where we can leverage our brand new (on iOS) Service Workers API - we are able to cache network requests (eg. API calls) there.


To take advantage of caching with service worker, you could use example code provided by Google. Make sure that you've read the linked article to be aware of pros and cons of that template.

Code
// source: https://googlechrome.github.io/samples/service-worker/basic/ /* Copyright 2016 Google Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Names of the two caches used in this version of the service worker. // Change to v2, etc. when you update any of the local resources, which will // in turn trigger the install event again. const PRECACHE = 'precache-v1'; const RUNTIME = 'runtime'; // A list of local resources we always want to be cached. const PRECACHE_URLS = [ 'index.html', './', // Alias for index.html 'styles.css', '../../styles/main.css', 'demo.js' ]; // The install handler takes care of precaching the resources we always need. self.addEventListener('install', event =&gt; { event.waitUntil( caches.open(PRECACHE) .then(cache =&gt; cache.addAll(PRECACHE_URLS)) .then(self.skipWaiting()) ); }); // The activate handler takes care of cleaning up old caches. self.addEventListener('activate', event =&gt; { const currentCaches = [PRECACHE, RUNTIME]; event.waitUntil( caches.keys().then(cacheNames =&gt; { return cacheNames.filter(cacheName =&gt; !currentCaches.includes(cacheName)); }).then(cachesToDelete =&gt; { return Promise.all(cachesToDelete.map(cacheToDelete =&gt; { return caches.delete(cacheToDelete); })); }).then(() =&gt; self.clients.claim()) ); }); // The fetch handler serves responses for same-origin resources from a cache. // If no response is found, it populates the runtime cache with the response // from the network before returning it to the page. self.addEventListener('fetch', event =&gt; { // Skip cross-origin requests, like those for Google Analytics. if (event.request.url.startsWith(self.location.origin)) { event.respondWith( caches.match(event.request).then(cachedResponse =&gt; { if (cachedResponse) { return cachedResponse; } return caches.open(RUNTIME).then(cache =&gt; { return fetch(event.request).then(response =&gt; { // Put a copy of the response in the runtime cache. return cache.put(event.request, response.clone()).then(() =&gt; { return response; }); }); }); }) ); } });

After setting proper paths to assets which you'd like to cache and registering service worker, you should be good to go (offline). It's also good to indicate that the app is offline - you can add online/offline events listeners to display a proper notification.

Code
componentDidMount() { window.addEventListener('online', () =&gt; this.setOnlineStatus(true)); window.addEventListener('offline', () =&gt; this.setOnlineStatus(false)); } componentWillUnmount() { window.removeEventListener('online'); window.removeEventListener('offline'); } setOnlineStatus = isOnline =&gt; this.setState({ online: isOnline })


Issues to be solved

These tips will help you to provide nearly native UX for your PWA on iOS. But of course, there are still some issues that could be solved. We still don't have support for:


- Background synchronization

- Push notifications

- Some APIs like TouchID, ARKit, In App Payments, Split View on iPad

- Orientation lock

- Statusbar color change

- Proper app screen in task manager (which would show current app screen, not splash image)


Also, you should keep in mind that caches and any locally-stored data is not shared between Safari, WebView, and Web.App. So after adding your app to home screen user will have to e.g. log in again, which could cause bad UX in some cases.


source: https://www.netguru.co/codestories/few-tips-that-will-make-your-pwa-on-ios-feel-like-native