Why (and how) I ported my iOS app to Vue.js
I recently ported a reasonably complex iOS app to a Vue.js Single Page Application (you are using it right now), and I thought I'd write down some of the thinking and challenges behind this port.
It's a food tracking app that I wrote for my own personal use, because I wasn't satisfied with the app I had been using (one of my biggest gripes was how many taps it took just to add a food to the diary. Well, that, and the frequent crash-on-launch issues). I've been programming for iOS for a decade, and writing Python and administering databases for longer than that, so this isn't as ambitious as it might sound.
Many Apps No Longer Need to Be Native
Almost every tech business has an app, or wants one, and I've written many myself. I was one of those people that scoffed at the idea of trying to deliver a good experience on mobile without going through the process of debugging memory leaks in an Objective-C or Swift code base. In the past, this was defintely true – browsers just weren't up to the task of providing the speed, UI, access to local storage, etc. that one got with a native app.
But times have changed. Some things that I thought of as purely the domain of native apps are now, in many cases, easier to do in the browser. Take local storage, for example. On iOS, you can set up Core Data, or write everything to some huge prefs file, and then write a bunch of wrapper code. But it'd be hard to beat
localStorage.setItem("foo", "bar") for ease of use (and debugging!).
One thing I definitely wanted in my app was the ability to scan bar codes. This is extremely easy to do in iOS (it's built into the camera view), but with a few modifications to quaggaJS I was able to come up with something I'm happy with (try it yourself, if you have a food barcode handy). That said, I don't have nearly the control over the camera that I'd have natively, but I just don't need image filters or the like for my application. My biggest disappointment was that the barcode scanner doesn't work on iOS if the app is installed to the home screen (Apple doesn't allow the use of
getUserMedia in a PWA context for some reason).
In short, unless you are writing a game, or need access to a lot of custom UI controls, or want to process photos or videos, it makes sense to ask yourself "Does this really need to be a native app?" I wish I hadn't wasted nearly a month (longer than it took to write the Vue app) trying to get Siri support to work for adding foods to my diary (it was just never reliable enough to use*).
Easier to Distribute
The Apple App Store requires that every app be reviewed before it can be downloaded by users, and this applies even to closed beta releases. This makes quick iteration and testing difficult. But even once this hurdle has been overcome, and the app is in the App Store, it's still much more work to get someone to download and launch an app than it is to click on a link. Everyone will tell you "sure, I'll test your app!", but many won't unless you make it very easy.
Ease of Building UI
There's no getting around the fact that building even basic UI on iOS is a pain. Making a form to submit data, and making it look OK on multiple devices, requires an incredible amount of boilerplate (
UITextField uses an integer(!) as an identifier). This situation should be improved with SwiftUI which will be released this year, but using HTML/CSS on a dev server with hot reloading works well for me.
SEO and Deep Links
On this front, a native app can't compete with a web app. Discovery of content inside apps, although work is being done to improve it, is still difficult. A web app, with an URL for every view, can get exposure to many more users than a native app.
What about SEO with an SPA? I haven't had any trouble getting my pages indexed by the GoogleBot even though the app does not do any server side rendering (SSR)**. The pages are indexed as if they were rendered server-side, including the META tags (e.g. title, description) that I add after Vue.js loads and the API call is complete. I think one thing that might help here is that my app is very small, each page makes only a single API call, and that API calls returns very quickly (often in less than 100ms when the crawler is in the US). The site usually scores between 90-98 (out of 100) when using the Google PageSpeed Insights tool.
At the time I started the port to Vue, I already had a nearly-complete iOS app and a fully functional API. The back-end for the app is a Django server using Django Rest Framework with a Postgres database. The CSS framework is Buefy, but I don't use the columns support. I used CSS Grid to handle the responsive design. Everything is on AWS, with the app being served as static files from S3. Performance is quite good, even though I'm still using the Free Tier.
As for why I chose Vue, and not, say, React or Angular, it came down to the learning curve and what I felt comfortable using. I didn't like JSX very much, and Angular seemed complex, so I settled on Vue pretty quickly. I can't say I regret the decision – I've been very productive with Vue and had a working app that I could play with, including type-ahead text suggestions, etc. within a day of firing up the CLI.
Speaking of the Vue CLI, I ran
vue create practical-diet, accepted most of the defaults ("what's Webpack? I guess I'm going to find out"), including PWA support, and got to coding. I was surprised at how easy it was.
PWA support was on my "wish list" before I started the project, but with the CLI I got it for free. Other than a few tweaks I made to
service-worker.js to notify a user when there's a new version of the app, it worked out of the box. And my Lighthouse scores (the "audit" pane in Chrome dev tools) are quite good:
I'm extremely happy with the way this turned out. It has exceeded my expectations, and in many ways the new app is better than the iOS app it replaces. For example, if you add some foods to your diary for "Lunch", and then tap on the "Lunch" title in the "Meals" view, it slides down a nutrition label just for that meal. This never existed in the iOS app. Mucking about with toggles changing the number of rows in a table in iOS can easily result in crashes (if you add/remove items from the array and this happens out of sync with the system's call to
tableView(_:numberOfRowsInSection:) you're dead. I've worked on apps that were written by Fortune 500 companies that contained these sorts of errors).
The reason, I think, that many companies still focus on their native apps, and users prefer them, is that their mobile sites/apps aren't very good. Hopefully all the new JS frameworks, and the increasingly capable mobile browsers, will change that.
* Although I did get a few improvements for the app from my battle with Siri. For example, if you say "four and a half cups of flour" to Siri, the OS will send the string "4 1/2 cups of flour" to your app. Even worse, "2 oreo cookies" is resolved as "two oreo cookies". Not the friendliest way for a programmer to receive numbers ("4.5" and "2" would have been better).
So, I built a parser to handle these, and it's still in the backend. Try the search box – "4 1/2 cups of flour" and "twenty seven oreo cookies" produce the desired results. Even arbitrary fractions ("22/7 cherry pies" ≈ "π pies") work. I never did bother to solve for the fact that Siri often confuses "for" and "four", "to" and "two", etc.