Full Screen Web Apps on iOS

February 16, 2016Category: TutorialTags: , , , ,
Safari Add to Homescreen-Hero

You’ve created your web application, added your manifest.json file, created your icons, and added your site to your Android and iOS test devices. Opening the app on Android works perfectly. The app opens like a native app (no browser chrome), stays that way through page changes, and keeps it’s state when minimized. However, something bad happens on iOS. The first link you click jumps you out of your app and into Safari. And even if you avoid that problem by using JS for page changes, the app returns to the homepage on every opening, because the state isn’t saved.

I had to deal with all of this over the past few days as I worked on polishing up Kepler. Everything was working perfectly until I started testing on an iPad. However, I couldn’t stay inside the app. Luckily, this problem has a lot of information on the web. It’s just a design decision on Apple’s part. All links open in Safari, rather than stay in the web app. The solution here is to intercept link clicks and use JS to change the page.

However, that exposes a second problem which I couldn’t find quite as much about. Unlike Android, iOS clears the state of web apps the moment they are minimized. If you open a web app, click on the home button, and then reopen the app … you are back on your landing page.

We can fix this using a combination of localStorage and sessionStorage. We need to take different actions on the very first page (first page after clicking the icon to open the app), and on all other pages. We use sessionStorage for this, because sessionStorage is cleared when the app is closed. We then use localStorage to save the state itself, because this will persist after the app is closed.

Rather than give you a large chunk of code that you can just copy without understanding, lets take it piece by piece first so that I can explain what is going on. First:

var insideApp = sessionStorage.getItem('insideApp')

We first check to see if there is a sessionStorage key/value pair for ‘insideApp’. When the app is first opened, there is no sessionStorage at all (because sessionStorage is cleared after every session, which is why it’s useful here), so this key will return null. Combined with an if statement, we now know if the app has just been opened or if we are already inside it.

if ( insideApp ) {
    // WE ARE INSIDE THE APP; THIS PAGE ISN'T THE FIRST PAGE
} else {
    // THIS IS THE FIRST PAGE SINCE OPENING THE APP
}

Now that we know where we are, we can take actions based on that. If we are inside the app, then all we want to do is save the state. We’ll use localStorage here, rather than sessionStorage, because we need to be able to access this information after the app is closed and reopened.

if ( insideApp ) {
    localStorage.setItem('returnToPage', location);
} else {
    // THIS IS THE FIRST PAGE SINCE OPENING THE APP
}

So now every time we navigate to a new page, we save that location into localStorage. Next we need to define what to do on the first page, and this is where redirection comes along. We know that the app has just been opened. If it had previously been opened, we want to restore the previous state. If it hadn’t, we want to continue to the home page.

We check localStorage for the value that we set on inner pages, and if it exists, we go to that page via JS. If it doesn’t, we do nothing. Additionally, we set a sessionStorage value, that we are in the app. All subsequent pages now know this and know to save their state.

if ( insideApp ) {
    localStorage.setItem('returnToPage', location);
} else {
    var returnToPage = localStorage.getItem('returnToPage');
    if ( returnToPage ) {
        window.location = returnToPage;
    }
    sessionStorage.setItem('insideApp', true);
}

That is all there is to saving state. As I said earlier though, we have to combine that with catching and replacing link clicks, which is where the second half comes in. We add an event listener for clicks anywhere on the document, and then replace the default action with changing over the location via JS.

document.addEventListener('click', function(event) {
    var clickedLink = event.target;
    while (!(stop).test(clickedLink.nodeName)) {
        clickedLink = clickedLink.parentNode;
    }
    if('href' in clickedLink && ( clickedLink.href.indexOf('http') || ~clickedLink.href.indexOf(location.host) ) ) {
        event.preventDefault();
        window.location = clickedLink.href;
    }
},false);

Putting it all together in a self-executing anonymous function, we have (and using the navigator object provided by the browser to tell that we are in an iOS full screen web app):

(function(document,navigator,standalone) {
    if ((standalone in navigator) && navigator[standalone]) {
        var insideApp = sessionStorage.getItem('insideApp'), location = window.location, stop = /^(a|html)$/i;
        if ( insideApp ) {
            localStorage.setItem('returnToPage', location);
        } else {
            var returnToPage = localStorage.getItem('returnToPage');
            if ( returnToPage ) {
                window.location = returnToPage;
            }
            sessionStorage.setItem('insideApp', true);
        }
        document.addEventListener('click', function(event) {
            var clickedLink = event.target;
            while (!(stop).test(clickedLink.nodeName)) {
                clickedLink = clickedLink.parentNode;
            }
            if('href' in clickedLink && ( clickedLink.href.indexOf('http') || ~clickedLink.href.indexOf(location.host) ) ) {
                event.preventDefault();
                location.href = clickedLink.href;
            }
        },false);
    }
})(document,window.navigator,'standalone');