{"id":2990,"date":"2017-09-27T16:00:14","date_gmt":"2017-09-27T14:00:14","guid":{"rendered":"https:\/\/beebole.com\/blog\/?p=2990"},"modified":"2025-01-22T12:09:21","modified_gmt":"2025-01-22T11:09:21","slug":"building-pwa-web-app-android-ios","status":"publish","type":"post","link":"https:\/\/beebole.com\/blog\/building-pwa-web-app-android-ios","title":{"rendered":"Building a PWA for Android and iOS: Tutorial and live example ?"},"content":{"rendered":"<p>Mobile web apps (known as <strong>Progressive Web Apps<\/strong> or <strong>PWA<\/strong>) can be a cheaper and totally viable replacement to native apps in many domains. As it&#8217;s been proved elsewhere, native apps require a costly launch and maintenance cycle. Google is betting strong on PWAs by implementing <strong>Service Workers<\/strong> and although iOS is not reacting that quickly on this trend, there are so many things you can do for both environments already.<\/p>\n<p>The goal was to deliver an application, in our case a web app to manage our <a class=\"highlighted-link bbl-link-hs bbl-link-hs-v-1\" title=\"BeeBole Timesheet\" href=\"https:\/\/beebole.com\" data-abc=\"true\"><span> timesheet solution<svg width=\"17\" height=\"18\" viewBox=\"0 0 17 18\" fill=\"none\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M11.25 0.875H15.625C15.7908 0.875 15.9497 0.940848 16.0669 1.05806C16.1842 1.17527 16.25 1.33424 16.25 1.5V5.875C16.25 6.04076 16.1842 6.19973 16.0669 6.31694C15.9497 6.43415 15.7908 6.5 15.625 6.5C15.4592 6.5 15.3003 6.43415 15.1831 6.31694C15.0658 6.19973 15 6.04076 15 5.875V3.00833L4.81667 13.1917C4.69819 13.3021 4.54148 13.3622 4.37956 13.3593C4.21765 13.3565 4.06316 13.2909 3.94865 13.1764C3.83414 13.0618 3.76854 12.9074 3.76569 12.7454C3.76283 12.5835 3.82293 12.4268 3.93333 12.3083L14.1167 2.125H11.25C11.0842 2.125 10.9253 2.05915 10.8081 1.94194C10.6908 1.82473 10.625 1.66576 10.625 1.5C10.625 1.33424 10.6908 1.17527 10.8081 1.05806C10.9253 0.940848 11.0842 0.875 11.25 0.875ZM2.5 4.625C2.16848 4.625 1.85054 4.7567 1.61612 4.99112C1.3817 5.22554 1.25 5.54348 1.25 5.875V14.625C1.25 14.9565 1.3817 15.2745 1.61612 15.5089C1.85054 15.7433 2.16848 15.875 2.5 15.875H11.25C11.5815 15.875 11.8995 15.7433 12.1339 15.5089C12.3683 15.2745 12.5 14.9565 12.5 14.625V7.75C12.5 7.58424 12.5658 7.42527 12.6831 7.30806C12.8003 7.19085 12.9592 7.125 13.125 7.125C13.2908 7.125 13.4497 7.19085 13.5669 7.30806C13.6842 7.42527 13.75 7.58424 13.75 7.75V14.625C13.75 15.288 13.4866 15.9239 13.0178 16.3928C12.5489 16.8616 11.913 17.125 11.25 17.125H2.5C1.83696 17.125 1.20107 16.8616 0.732233 16.3928C0.263392 15.9239 0 15.288 0 14.625V5.875C0 5.21196 0.263392 4.57607 0.732233 4.10723C1.20107 3.63839 1.83696 3.375 2.5 3.375H9.375C9.54076 3.375 9.69973 3.44085 9.81694 3.55806C9.93415 3.67527 10 3.83424 10 4C10 4.16576 9.93415 4.32473 9.81694 4.44194C9.69973 4.55915 9.54076 4.625 9.375 4.625H2.5Z\"\/><\/svg><\/span><\/a>, that works for all main browsers installed on top of Android and iOS. The application is currently used on multiple browser\/devices, including but not limited to Chrome, Firefox and Opera running on iPhones, iPads on iOS 10.X and Android 5.X+ devices.<\/p>\n<p>For <strong>TL;DR<\/strong> people out there, there is a live working example including all coding and many self explanatory comments. Check the Quick Links.<\/p>\n<h2 id=\"reason\">Why building an offline web app<\/h2>\n<p>There are immediate advantages from developing a web app vs a native app:<\/p>\n<ul>\n<li>Unified development skillset.<\/li>\n<li>Easier deployment and automatic updates.<\/li>\n<li>Extensive collection of open source web libraries.<\/li>\n<li>No dependency on stores nor operating system.<\/li>\n<\/ul>\n<p>But we were also well aware of the <strong>existing constraints<\/strong>:<\/p>\n<ul>\n<li>Market familiarity to look for apps on the two main app stores<\/li>\n<li>Limitations to certain device capabilities (<a title=\"Web APIs\" href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/WebAPI\" data-abc=\"true\" target=\"_blank\" rel=\"noopener\">Web APIs<\/a> are catching up though).<\/li>\n<\/ul>\n<p>But our reason could not be simpler: We strongly believe in the future of web apps. Google is certainly going in that direction. And did you know Steve Jobs himself shared <a href=\"https:\/\/9to5mac.com\/2011\/10\/21\/jobs-original-vision-for-the-iphone-no-third-party-native-apps\/\" rel=\"nofollow noopener\" data-abc=\"true\" target=\"_blank\">the same vision<\/a>? A <a href=\"https:\/\/www.youtube.com\/watch?v=IgckqIjvR9U&amp;t=10s\" rel=\"nofollow noopener\" data-abc=\"true\" target=\"_blank\">very powerful presentation<\/a>\u00a0on web apps from Patrick Kettner (Edge team @ Microsoft) helped us decide.<\/p>\n<h2 id=\"stack\">The teammates: The web app stack<\/h2>\n<p>We have shared this path with a few dependable friends: The rookie <strong>Service Worker<\/strong> and the somehow more veteran &#8211; and sometimes misunderstood <strong>AppCache<\/strong>.<\/p>\n<p><strong>IndexedDB<\/strong> came to the rescue to help facilitate conversations with any of the other two, and to set up the basis for a common ground (sic).<\/p>\n<h2 id=\"game\">The game: How to make your app a PWA<\/h2>\n<h3 id=\"indexedDB\">1. Using IndexedDB for PWA<\/h3>\n<p>Although there are several <strong>libraries<\/strong> out there to help you use IndexedDB (Debie, jquery-indexeddb, pouchDB, db.js and more), performance and reliability are among our top priorities so we decided to leverage the standard IndexedDB API following the <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/IndexedDB_API\/Using_IndexedDB\" target=\"_blank\" rel=\"nofollow noopener noreferrer\" data-abc=\"true\">MDN documentation<\/a> to have absolute control over the final delivery. This was quite a straightforward process and we could adjust the development to our specific needs.<\/p>\n<p>Although the IndexedDB coding may be worth a second post to share our hits and misses, it works like a charm in both Android and iOS and it is not the goal for this article.<\/p>\n<h3 id=\"setup\">2. Setting it up like a native app<\/h3>\n<p>Both Android and iOS provide the mechanism to add an <strong>icon to the home screen<\/strong>, so that users can initiate the app with just one touch, like any other native app. Applications run from the home page will use the entire screen and will hide the browser navigation bar. This feature is well documented on <a href=\"https:\/\/developers.google.com\/web\/fundamentals\/engage-and-retain\/app-install-banners\/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\" data-abc=\"true\">Web fundamentals<\/a> for Android and on <a href=\"https:\/\/webdesign.tutsplus.com\/articles\/quick-tip-give-your-website-an-ios-home-screen-icon--webdesign-10067\" target=\"_blank\" rel=\"nofollow noopener noreferrer\" data-abc=\"true\">tuts+<\/a> for iOS. This is an optional step but highly recommended for your web app to be initiated as any other mobile app. It is not however the purpose of our tutorial.<\/p>\n<h3>3. Working offline<\/h3>\n<p><strong>Making the app work offline for Android<\/strong> is quite straight forward using Service Workers (HTTPS being the only requirement). <strong>AppCache setup is also simple<\/strong> but required a bit more caring. Working offline is also what used to make a big difference between a native and a PWA. Not anymore. This is the real deal and the plan is to explain below how we made it work for us. Keep reading. We provide a step by step guide for both scenarios.<\/p>\n<h4 id=\"service\">3.1 <strong>Service Workers<\/strong> for PWA<\/h4>\n<p>Before you continue, you need to be familiar with how promises work. If that is not yet the case, a classic read would be <a href=\"https:\/\/developers.google.com\/web\/fundamentals\/getting-started\/primers\/promises\" target=\"_blank\" rel=\"nofollow noopener noreferrer\" data-abc=\"true\">this article from Mr. Service Worker<\/a> (aka Jake Archibald). However,ap in this case, I found <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Guide\/Using_promises\" target=\"_blank\" rel=\"nofollow noopener noreferrer\" data-abc=\"true\">the MDN tutorial<\/a> more self explanatory. Now, to start implementing a service worker solution, an option is to go to <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/Service_Worker_API\/Using_Service_Workers\" target=\"_blank\" rel=\"nofollow noopener noreferrer\" data-abc=\"true\">MDN again<\/a>, but my personal recommendation is to have a look at <a href=\"https:\/\/developers.google.com\/web\/fundamentals\/getting-started\/primers\/service-workers\" target=\"_blank\" rel=\"nofollow noopener noreferrer\" data-abc=\"true\">Matt Gaunt\u2019s introduction to Service Workers<\/a>.<\/p>\n<p>Here\u2019s a quick summary of the steps we followed for our implementation.<\/p>\n<p><strong>1. <em>Register the service worker<\/em><\/strong><\/p>\n<p>The browser needs to support serviceWorker, so make sure to add the check. This code is to be implemented as starting point of your application (<em>index.html<\/em> in our example).<\/p>\n<p><a href=\"https:\/\/gist.github.com\/beebole\/26632ca09b5c85da5b4d9b570e490bdc\" data-abc=\"true\" target=\"_blank\" rel=\"noopener\">https:\/\/gist.github.com\/beebole\/26632ca09b5c85da5b4d9b570e490bdc<\/a><\/p>\n<p><strong>2. <em>Enable offline usage at page load<\/em><\/strong><\/p>\n<p>In order to work offline, you need to cache the files that the application needs to run. You need to create an independent file (ex:<em>sw.js<\/em>) containing the service worker logic. This file needs to be placed at the lowest folder including all the elements that you want to use offline (i.e. if you have files within multiple folders that you want to cache, the sw file needs to be placed on the root folder including all those upper folders).<\/p>\n<p>By default, the SW will be installed but will only become active the next time the user loads the app. It is possible however to precache all pages from the application while installing the Service worker (even before it becomes active).<\/p>\n<p>Simply capture both the install and activate events for the service worker and add the predefined list of elements to the cache.<\/p>\n<p>This step needs to be performed on the SW code itself.<\/p>\n<p>Declare a variable to specify the list of resources to be cached:<\/p>\n<p><a href=\"https:\/\/gist.github.com\/beebole\/c8f60bfe6f56f5a400050586dc70ef2b\" data-abc=\"true\" target=\"_blank\" rel=\"noopener\">https:\/\/gist.github.com\/beebole\/c8f60bfe6f56f5a400050586dc70ef2b<\/a><\/p>\n<p>Define handler for the install event and skip the waiting:<\/p>\n<p><a href=\"https:\/\/gist.github.com\/beebole\/878764ac495a1c4521c50c849101fc71\" data-abc=\"true\" target=\"_blank\" rel=\"noopener\">https:\/\/gist.github.com\/beebole\/878764ac495a1c4521c50c849101fc71<\/a><\/p>\n<p>Cache list of predefined resources during activate event, using the cache API:<\/p>\n<p><a href=\"https:\/\/gist.github.com\/beebole\/7181904aaeabd9b9f2a83d9b07008159\" data-abc=\"true\" target=\"_blank\" rel=\"noopener\">https:\/\/gist.github.com\/beebole\/7181904aaeabd9b9f2a83d9b07008159<\/a><\/p>\n<p>Finally, make sure the current becomes the active service worker:<\/p>\n<p><a href=\"https:\/\/gist.github.com\/beebole\/e138db94e6c12e4a34d3a8ee747421d8\" data-abc=\"true\" target=\"_blank\" rel=\"noopener\">https:\/\/gist.github.com\/beebole\/e138db94e6c12e4a34d3a8ee747421d8<\/a><\/p>\n<p>Even if the user goes offline after loading the page, they will be able to navigate through the entire application (as long as elements are properly defined in the urlsToCache array).<\/p>\n<p><strong><em>3. Retrieve resources from the cache<\/em><\/strong><br \/>\nAdd a listener to all fetch actions from the browser. The SW has the ability to capture the request before sending the response to the browser:<\/p>\n<p><a href=\"https:\/\/gist.github.com\/beebole\/2843b5fbb9ea219add6fcafd39eebc0d\" data-abc=\"true\" target=\"_blank\" rel=\"noopener\">https:\/\/gist.github.com\/beebole\/2843b5fbb9ea219add6fcafd39eebc0d<\/a><\/p>\n<p>Please note that <em>self<\/em> in this case does not correspond to <em>window<\/em>, which does not exist in the service worker context, but to the service worker instance, which is independent from the browser window. Next, use the event to send the response to the browser:<\/p>\n<p><a href=\"https:\/\/gist.github.com\/beebole\/0788485dd7d6051610137c089aa4e908\" data-abc=\"true\" target=\"_blank\" rel=\"noopener\">https:\/\/gist.github.com\/beebole\/0788485dd7d6051610137c089aa4e908<\/a><\/p>\n<p>At this stage, we can send the requested object from the cache, if it is already there:<\/p>\n<p><a href=\"https:\/\/gist.github.com\/beebole\/11f2e23214a81f83da1588a0b7d28d5b\" data-abc=\"true\" target=\"_blank\" rel=\"noopener\">https:\/\/gist.github.com\/beebole\/11f2e23214a81f83da1588a0b7d28d5b<\/a><\/p>\n<p>Or we can request it from the network otherwise:<\/p>\n<p><a href=\"https:\/\/gist.github.com\/beebole\/19e788956465a65b8864889387ef241f\" data-abc=\"true\" target=\"_blank\" rel=\"noopener\">https:\/\/gist.github.com\/beebole\/19e788956465a65b8864889387ef241f<\/a><\/p>\n<p><strong><em>4. Cache all resources<\/em><\/strong><\/p>\n<p>It is optional to add some logic that will cache any other resource accessed by the user while navigating through the app. You even have the option to skip the previous step and only cache resources as the user accesses them.<\/p>\n<p>Within the fetch handled above, if the resource is not already in the cache, we may want to save it there for a later use. For that, we first capture the response from the prior promise:<\/p>\n<p><a href=\"https:\/\/gist.github.com\/beebole\/5958d95abdc7d7ad0c214f5d600fd10c\" data-abc=\"true\" target=\"_blank\" rel=\"noopener\">https:\/\/gist.github.com\/beebole\/5958d95abdc7d7ad0c214f5d600fd10c<\/a><\/p>\n<p>And then use the cache API to save the response:<br \/>\n<a href=\"https:\/\/gist.github.com\/beebole\/71c96387da81e36b6602269d0ae09621\" data-abc=\"true\" target=\"_blank\" rel=\"noopener\">https:\/\/gist.github.com\/beebole\/71c96387da81e36b6602269d0ae09621<\/a><\/p>\n<p><em>name_your_cache_container<\/em> being the name of the cache that you can see from the Web Console when debugging your app. You can use any string there.<\/p>\n<p><strong><em>5. Manage updates<\/em><\/strong><\/p>\n<p>If there is a change in the <em>sw.js<\/em> file, the next time the user loads the app the new SW will be identified and loaded on a waiting state, until the app closes again so the older SW version can be released. By default also, any change to any cached page will not be detected by the app as it goes cache-first for performance reasons.<\/p>\n<p>As result, any change to any web page that you want to be detected by the app needs to be accompanied by a change to the SW file (<em>sw.js<\/em>) and it will not be captured by the browser until the user refreshes the app.<\/p>\n<p>It is possible however to force this refresh when any change to the SW is detected. (Some authors prefer to display a message informing the user of the new updates and let them decide whether to refresh or not).<\/p>\n<p>In order to automatically refresh, make sure the service worker is updated whenever a new version is available. Include this check in your web page (<em>index.html<\/em> in our example), right after the SW registration process:<\/p>\n<p><a href=\"https:\/\/gist.github.com\/beebole\/1285f8f32e0de8d21e5ec482629caf22\" data-abc=\"true\" target=\"_blank\" rel=\"noopener\">https:\/\/gist.github.com\/beebole\/1285f8f32e0de8d21e5ec482629caf22<\/a><\/p>\n<p>To test this feature, you will have to deploy this app to your own server, make a change to the html page AND the sw.js file, deploy and reload.<\/p>\n<h4 id=\"appcache\">3.2 <strong>AppCache<\/strong> management<\/h4>\n<p>At this point, you may have already read <a href=\"https:\/\/alistapart.com\/article\/application-cache-is-a-douchebag\" target=\"_blank\" rel=\"nofollow noopener noreferrer\" data-abc=\"true\">Jake\u2019s dissertation about the AppCache<\/a>. Additionally, if you read the <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/Window\/applicationCache\" target=\"_blank\" rel=\"nofollow noopener noreferrer\" data-abc=\"true\">AppCache MDN documentation<\/a>, you might be discouraged by the first paragraph:<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"alignnone wp-image-10766 size-full\" src=\"https:\/\/beebole.com\/blog\/wp-content\/uploads\/2017\/09\/appcache-pwa-web-app.png\" alt=\"AppCache for PWA, web app\" width=\"948\" height=\"342\" title=\"\" srcset=\"https:\/\/beebole.com\/blog\/wp-content\/uploads\/2017\/09\/appcache-pwa-web-app.png 948w, https:\/\/beebole.com\/blog\/wp-content\/uploads\/2017\/09\/appcache-pwa-web-app-700x253.png 700w, https:\/\/beebole.com\/blog\/wp-content\/uploads\/2017\/09\/appcache-pwa-web-app-768x277.png 768w\" sizes=\"auto, (max-width: 948px) 100vw, 948px\" \/><\/p>\n<p>HOWEVER, and this is a big however, <strong>if you want to deploy a web application with offline capabilities on iOS<\/strong>, AppCache is so far your only option, and as we learned, sometimes painfully, it works just fine.<\/p>\n<p>Below are the required steps we followed to make it work.<\/p>\n<p><strong><em>1. Define list of items to be cached<\/em><\/strong><\/p>\n<p>This is a <strong>mandatory<\/strong> step with AppCache and it can be defined on an independent file called appcache manifest. You can name it the way you want but a common naming convention is to use: <em>offline.appcache<\/em>.<\/p>\n<p>Mandatory line to start your file with:<\/p>\n<p><code>CACHE MANIFEST<\/code><\/p>\n<p>Add a line to maintain the version. This will help the browser to identify when the cache files need to be refreshed. You can enter any text, but the recommendation is to keep a numeric counter to keep track of your versioning internally.<\/p>\n<p>Use \u2018#\u2019for comments within the appcache manifest file.<\/p>\n<p><code># BeeBole Mobile AppCache v251<\/code><\/p>\n<p>Define the list of files to be cached. You can use absolute or relative paths here. The list needs to be defined by CACHE:<\/p>\n<p><code>CACHE:<br \/>\n.<br \/>\nindex.html<br \/>\nmobile.js<br \/>\nmobile.css<\/code><\/p>\n<p>If all your resources can be retrieved from the cache while offline, the following line applies. This can be modified if specific resources need to be white listed and retrieved from the network even when offline (sic):<\/p>\n<p><code>NETWORK:<br \/>\n*<\/code><\/p>\n<p>Finally, it is possible to add a fallback resource in case any resource is not available from the cache while offline. Most typical usage is to display an offline image to indicate that there is no access to the network resource. It works by pairs, indicating a fallback resource for each URI. To use the same fallback for all resources, you can use \u2018\/\u2019.<\/p>\n<p><code>FALLBACK:<br \/>\n\/ offline.png<\/code><\/p>\n<p>And this is how it looks all together:<\/p>\n<p><code>CACHE MANIFEST<br \/>\n# BeeBole Mobile AppCache v251<br \/>\nCACHE:<br \/>\n.<br \/>\nindex.html<br \/>\nmobile.js<br \/>\nmobile.css<br \/>\nNETWORK:<br \/>\n*<br \/>\nFALLBACK:<br \/>\n\/ offline.png<\/code><\/p>\n<p><strong><em>2. Call the appcache manifest from the web app. <\/em><\/strong><br \/>\nSimply include the following code within your html page, with the proper manifest file location:<\/p>\n<p><a href=\"https:\/\/gist.github.com\/beebole\/4d07f4d4efbfd614db9e187396f7b221\" data-abc=\"true\" target=\"_blank\" rel=\"noopener\">https:\/\/gist.github.com\/beebole\/4d07f4d4efbfd614db9e187396f7b221<\/a><\/p>\n<p><span style=\"font-weight: 400;\">The problem here is that the <em>offline.appcache<\/em> file needs to be available on page load time, which may not always be the case. There is a trick (as proposed by Patrick Kettner) to load the appcache dynamically, by injecting it in a hidden iframe<\/span><span style=\"font-weight: 400;\">. <\/span><\/p>\n<ul>\n<li style=\"font-weight: 400;\"><span style=\"font-weight: 400;\">Create an external file (ex: load-appcache.html) with just the following content<\/span><span style=\"font-weight: 400;\"><br \/>\n<\/span><br \/>\n<a href=\"https:\/\/gist.github.com\/beebole\/2b38eadee78f4d248eb100b45bfaefe9\" data-abc=\"true\" target=\"_blank\" rel=\"noopener\">https:\/\/gist.github.com\/beebole\/2b38eadee78f4d248eb100b45bfaefe9<\/a><\/li>\n<\/ul>\n<ul>\n<li><span style=\"font-weight: 400;\">Replace the <\/span><i><span style=\"font-weight: 400;\">manifest=&#8221;offline.appcache\u201d<\/span><\/i><span style=\"font-weight: 400;\"> attribute from the main html file with the following logic whenever required within your javascript flow, to dynamically load the page calling the manifest within an invisible container.<\/span><br \/>\n<a href=\"https:\/\/gist.github.com\/beebole\/d27e8b5f372f13ae8b31c45adcabb460\" data-abc=\"true\" target=\"_blank\" rel=\"noopener\">https:\/\/gist.github.com\/beebole\/d27e8b5f372f13ae8b31c45adcabb460<\/a><\/li>\n<\/ul>\n<p><strong><em>3. Make sure the last version is used\u00a0<\/em><\/strong><\/p>\n<p><span style=\"font-weight: 400;\">Until this point, you are making sure that the appCache is refreshed whenever a change exists in the manifest file (<\/span><i><span style=\"font-weight: 400;\">remember that it is required for the manifest to be modified &#8211; increasing the version for instance &#8211; each time you modify any file within the application, or else the AppCache will keep feeding from the cached resources.)<\/span><\/i><span style=\"font-weight: 400;\">. <\/span><\/p>\n<p><span style=\"font-weight: 400;\">The application will use the previously cached resources until the page refreshes. You can force this refresh by detecting the change on the AppCache status.<\/span><span style=\"font-weight: 400;\"><br \/>\n<\/span><\/p>\n<p><span style=\"font-weight: 400;\">Simply a<\/span>dd a listener to reload the application if a new manifest has been identified. It is required for the manifest to be modified (increasing the version for instance) each time you modify any file within the application:<br \/>\n<a href=\"https:\/\/gist.github.com\/beebole\/fb926ad2a5405c955ca8df9aa714a85f\" data-abc=\"true\" target=\"_blank\" rel=\"noopener\">https:\/\/gist.github.com\/beebole\/fb926ad2a5405c955ca8df9aa714a85f<\/a><\/p>\n<h2 id=\"live\">Our Beebole mobile web app: Live example<\/h2>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignright wp-image-3019\" src=\"https:\/\/beebole.com\/blog\/wp-content\/uploads\/2017\/07\/beebole-timesheet-pwa-web-app.png\" alt=\"beebole-timesheet-pwa-web-app\" width=\"184\" height=\"327\" title=\"\" srcset=\"https:\/\/beebole.com\/blog\/wp-content\/uploads\/2017\/07\/beebole-timesheet-pwa-web-app.png 750w, https:\/\/beebole.com\/blog\/wp-content\/uploads\/2017\/07\/beebole-timesheet-pwa-web-app-700x1245.png 700w\" sizes=\"auto, (max-width: 184px) 100vw, 184px\" \/>As we stated at the beginning, it is possible to have a PWA working offline for both Android and iOS devices. You can see it for yourself: <a href=\"https:\/\/beebole-apps.com\/signup\/?utm_source=beeboleblog&amp;utm_medium=referral&amp;utm_campaign=signup&amp;utm_content=pwa\" target=\"_blank\" rel=\"noopener noreferrer\" data-abc=\"true\">register for a 30 day free trial<\/a>, no credit card required, and add our timesheet app to your homescreen.<\/p>\n<p>To make this whole post clearer, check out <a href=\"https:\/\/beebole.github.io\/mobile-app-demo\/\" target=\"_blank\" rel=\"noopener noreferrer\" data-abc=\"true\">https:\/\/beebole.github.io\/mobile-app-demo\/<\/a> where you can find all the above pieces working together.<br \/>\nBest way to test it is to see the app load, then go on airplane mode and navigate through the bunnies. You should see all bunnies even if you did not load them prior to going offline.<\/p>\n<p>Commented sources are available at <a href=\"https:\/\/github.com\/beebole\/mobile-app-demo\" target=\"_blank\" rel=\"noopener noreferrer\" data-abc=\"true\">Github<\/a>.<\/p>\n<h2 id=\"faq\">FAQs and tips<\/h2>\n<p><em>How do I make sure my Chrome console uses the last version of the files when developing and testing on localhost?<\/em><\/p>\n<p>Although the article describes how to make sure the browser will update the last version of the app when deployed to any server, it is true that localhost sometimes behaves its own way when developing with Chrome. The best way to make sure your app is using the last files is to manually clean up the cache files, which you can do via Chrome Console &#8211; Application &#8211; Cache Storage. Right click on the name of your cache (FILES_CACHE in our example above) and delete. If there is no arrow to expand the Cache Storage level, just right click there and select Refresh.<\/p>\n<p><em>Why is the browser saying that I am offline when I reload the page when on airplane mode?<\/em><\/p>\n<p>AppCache is very sensitive about the pages that are cached, and not very good in communicating when there is something it does not like. As soon as there is anything that the browser is trying to load and it has not been properly defined in the cache manifest, the application will fail entirely and the browser will state that you need a connection. Triple check that you have included all files. A common mistake is to forget about the favicon.ico if you have one in your server.<\/p>\n<hr \/>\n<p>So far, so good&#8230;Leave us a comment with your own questions or examples. Have you built your own mobile web app? Would you mind <a href=\"#comments\" data-abc=\"true\">sharing your thoughts<\/a> here with us?<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Mobile web apps (known as Progressive Web Apps or PWA) can be a cheaper and totally viable replacement to native apps in many domains. As it&#8217;s been proved elsewhere, native apps require a costly launch and maintenance cycle. Google is betting strong on PWAs by implementing Service Workers and although iOS is not reacting that [&hellip;]<\/p>\n","protected":false},"author":7,"featured_media":3018,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[7],"tags":[3963,4012],"class_list":["post-2990","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-beebole-news","tag-developers","tag-tutorials"],"acf":[],"_links":{"self":[{"href":"https:\/\/beebole.com\/blog\/wp-json\/wp\/v2\/posts\/2990","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/beebole.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/beebole.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/beebole.com\/blog\/wp-json\/wp\/v2\/users\/7"}],"replies":[{"embeddable":true,"href":"https:\/\/beebole.com\/blog\/wp-json\/wp\/v2\/comments?post=2990"}],"version-history":[{"count":11,"href":"https:\/\/beebole.com\/blog\/wp-json\/wp\/v2\/posts\/2990\/revisions"}],"predecessor-version":[{"id":12668,"href":"https:\/\/beebole.com\/blog\/wp-json\/wp\/v2\/posts\/2990\/revisions\/12668"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/beebole.com\/blog\/wp-json\/wp\/v2\/media\/3018"}],"wp:attachment":[{"href":"https:\/\/beebole.com\/blog\/wp-json\/wp\/v2\/media?parent=2990"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/beebole.com\/blog\/wp-json\/wp\/v2\/categories?post=2990"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/beebole.com\/blog\/wp-json\/wp\/v2\/tags?post=2990"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}