22

I have been struggling on iOS with something that works easily on Android: Getting my PWA to auto-update when there is a new version. I am not at all sure this is even possible on iOS. I have used vue.js and Quasar to build my app, and everything works out of the box on Android. Here is the (ugly, terrible) way things stand currently on the iOS version:

  1. I can check my own server for the version and compare it against the current one stored in my app (in indexedDB) and throw up a notice that there is a new version. So far so good.
  2. Other than having the user MANUALLY CLEAR THE SAFARI CACHE (!!) there is no way I can figure out how to programmatically clear the PWA cache from within the app or force an upload in another way.

So at this point I guess my questions are:

  1. Has ANYONE been able to get a PWA on iOS (11.3 or later) to auto-update when a new version is available?
  2. Is there a way to clear the (safari) app cache from within my PWA?

Obviously it is an incredibly awful user experience to notify the user that in order to update they must perform several steps outside of the app to be able to refresh it, but it seems this is where iOS stands at the moment unless I am missing something. Has anyone anywhere made this work?

Stephen
  • 8,038
  • 6
  • 41
  • 59
  • I think It should update in the background when you open the app. Then after you close the app, the next time you open it you should see the changes. Is that not happening? – Mathias Sep 07 '18 at 23:08
  • 2
    yes it SHOULD, but it doesn't. And I have seen zero evidence from anyone anywhere that this works over iOS. Do you have a working example? (This is a PWA with service worker we are talking about btw, so it should be caching as well.) – Stephen Sep 08 '18 at 00:07
  • No, I do not have a current iOS device. Just an old iPod, sorry. – Mathias Sep 08 '18 at 00:26
  • swiping app remove state for iOS 14 and for older version the phone should be restarted. – Ali Sep 11 '21 at 00:48

3 Answers3

34

After weeks and weeks of searching, I finally found a solution:

  1. I add a check for versionstring on the server, and return it to the app as mentioned above.

  2. I look for it in localtstorage (IndexedDB) and if I don’t find it, I add it. If I do find it, I compare versions and if there is a newer one on the server, I throw up a dialog.

  3. Dismissing this dialog (my button is labeled “update”) runs window.location.reload(true) and then stores the new versionstring in localstorage

Voila! My app is updated! I can't believe it came down to something this simple, I could not find a solution anywhere. Hope this helps someone else!

UPDATE SEPT 2019:

There were a few problems with the technique above, notably that it was bypassing the PWA service worker mechanisms and that sometimes reload would not immediately load new content (because the current SW would not release the page). I have now a new solution to this that seems to work on all platforms:

function forceSWupdate() {
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.getRegistrations().then(function (registrations) {
      for (let registration of registrations) {
        registration.update()
      }
    })
  }
}
forceSWupdate()

And inside my serviceworker I now throw up the dialog if there is an update, and do my location.reload(true) from there. This always results in my app being refreshed immediately (with the important caveat that I have added skipWaiting and clientsClaim directives to my registration).

This works on every platform the same, and I can programatically check for the update or wait for the service worker to do it by itself (although the times it checks vary greatly by platform, device, and other unknowable factors. Usually not more than 24 hours though.)

Alex Cory
  • 10,635
  • 10
  • 52
  • 62
Stephen
  • 8,038
  • 6
  • 41
  • 59
  • 3
    Not truly silent automatic updates though. If you're deploying your front-end multiple times a day, I could see users getting pretty tired of clicking the update button. Thoughts? – Chris G. Oct 17 '18 at 22:55
  • True, but it is a necessary workaround until Apple fixes this, and far better than doing nothing. – Stephen Oct 18 '18 at 05:01
  • Can your app make an intelligent decision regarding whether the user is "in the middle of something" or not? If so, it could auto-update if they are not doing anything. And if they _are_ doing something, it could wait for (say) up to 24 hours and then (if they are somehow still in the middle of something) put up a message requesting they update. – ProfDFrancis Nov 04 '18 at 18:29
  • I think it would really depend on the app. I could actually NOT throw up an update notice, and instead just force reload the app once a particular save had taken place, but I have chosen transparency (for the user) in this case because it is rare enough. – Stephen Nov 05 '18 at 10:39
  • Are you placing your forceSWupdate () in the version check method? – Tim Wickstrom Nov 13 '19 at 19:06
  • @TimWickstrom I am checking for the version string (in a file on the server, separate from the cache signature on the PWA itself) in the service worker which is called from wherever, to alert the user when there is a new version. the forceSWupdate() method just forces the service worker to run its updated method once it already knows there is an update. Separately I could just check the version in my file on the server and force an update if found to be different than local, but I haven't needed to. – Stephen Nov 14 '19 at 06:42
  • @TimWickstrom and tbh if you don't care about setting your own version strings (only checking when the cache sig is different on the PWA) then you can dispense with that altogether. – Stephen Nov 14 '19 at 06:43
  • can you provide a sample code I don't really get what are you saying – TheEhsanSarshar Feb 07 '20 at 14:30
  • @Ehsansarshar That is a code sample above. It does assume you are using WorkBox (https://developers.google.com/web/tools/workbox) for the service worker (as most people are). If that is the case, just try the code above, but instead of running your update, try logging your registration to the console to see what you get. For example, replace `registration.update()` with `console.log(registration)` – Stephen Feb 08 '20 at 16:02
  • Could you post your whole solution? really interested – abr Oct 21 '21 at 15:48
  • Does this still work? MDN is clear that parameterised reload method is not part of the standard. https://developer.mozilla.org/en-US/docs/Web/API/Location/reload – Chris Apr 27 '22 at 15:08
5

If anyone is still having issues with this, registration.update() did not work for me. I used the exact solution but when the version from my server did not match my local stored version, I had to unregister the service workers for it to work.

  if ('serviceWorker' in navigator) {
   await this.setState({ loadingMessage: 'Updating Your Experience' })
   navigator.serviceWorker.getRegistrations().then(function(registrations) {
    registrations.map(r => {
      r.unregister()
    })
   })
   await AsyncStorage.setItem('appVersion', this.state.serverAppVersion)
   window.location.reload(true)
  } 

Then when the app reloads, the service worker is reregistered and the current version of the app is visible on iOS safari browsers and 'bookmarked' PWAs.

jbone107
  • 101
  • 2
  • 7
3

Instead of prompting the user, that a new version is available, you can also extend the 'activate' Eventlistener to delete your old cache whenever you publish a new serviceworker version.

  1. Add version and name variables
var version = "v3" // increase for new version
var staticCacheName = version + "_pwa-static";
var dynamicCacheName = version + "_pwa-dynamic";
  1. Delete caches, when their names do not fit the current version:
self.addEventListener('activate', function(event) {
    event.waitUntil(
        caches.keys().then(function(cacheNames) {
            return Promise.all(
                cacheNames.filter(function(cacheName) {
                    if (!cacheName.startsWith(staticCacheName) &&
                        !cacheName.startsWith(dynamicCacheName)) {
                        return true;
                    }
                }).map(function(cacheName) {
                    console.log('Removing old cache.', cacheName);
                    return caches.delete(cacheName);
                })
            );
        })
    );
});

(credits: https://stackoverflow.com/a/45468998/14678591)

In order to make this work for iOS safari browsers and 'bookmarked' PWAs too, I just added the sligthly reduced function by @jbone107:

self.addEventListener('activate', function(event) {
    event.waitUntil(
        caches.keys().then(function(cacheNames) {
            return Promise.all(
                cacheNames.filter(function(cacheName) {
                    if (!cacheName.startsWith(staticCacheName) &&
                        !cacheName.startsWith(dynamicCacheName)) {
                        return true;
                    }
                }).map(function(cacheName) {
                    // completely deregister for ios to get changes too
                    console.log('deregistering Serviceworker')
                    if ('serviceWorker' in navigator) {
                        navigator.serviceWorker.getRegistrations().then(function(registrations) {
                            registrations.map(r => {
                                r.unregister()
                            })
                        })
                        window.location.reload(true)
                    }

                    console.log('Removing old cache.', cacheName);
                    return caches.delete(cacheName);
                })
            );
        })
    );
});

This way you just have to increase the version number and updating is done by the serviceworker automatically.