Today I have the pleasure of announcing my new app—Max reHIT Workout—on Product Hunt. Max reHIT Workout is an exercise app that guides you through interval workouts.
I won’t pitch the app here. I'll just say I’m proud of how it turned out and if you want an optimal algorithm for exercising, you might like it.
I know I haven’t been writing much lately. That's because there’s been very little evolution in software system architecture. It’s pretty much same thing, different day. In many ways that’s good, but it’s not interesting to write about.
This article, while definitely self serving, targets the choice of using a native iOS environment versus a cloud environment for an app. It’s a choice every developer must make. How do you make that choice? What are the implications? What choice would I make next time?
My previous several projects have largely been AWS based:
These used the typical AWS services: Lambda, DynamoDB, S3, Route 53, SES, SQS, Cloudfront, Cloud Watch, Cognito, API Gateway, HTTP API, Amazon Aurora, SNS, WebSockets, and so on. In addition, I use products like PayPal, Nginx, AdMob, Node, and Let's Encrypt as necessary.
The clients were Bootstrap based web front-ends or chatbot front-ends, with various degrees of sophistication on the back-end.
There’s just me, so I try to keep it as simple as possible. Rarely do I succeed. I’m sure I’m alone in that.
For Max reHIT Workout, my newest project, I wanted to try something completely different.
While I’ve found various parts of AWS straightforward to use, other parts not so much. More of my life has been spent battling Cognito into submission than I care to admit. The documentation and example code lack quality, to say the least.
Use AppSync you say? AppSync is too opinionated for me. It dictates using toolchains, processes, and services I don’t like using.
Amazon should make reusable, well documented, composable components instead of packaging it all up as a framework. But they went Ruby on Rails instead. That's great for a lot of people, but I want to make those choices for myself.
Also, I was tired of the web framework wars. CSS, Javascript, HTML—a constant battleground with more losers than winners.
I needed a change. I needed a sabbatical from the web.
So I decided to try something completely different: native iOS using SwiftUI.
My hunch was a native developer experience would be more delightful compared to a web + AWS experience. I was mostly right.
I'm not a complete iOS newbie. Many moons ago I created a simple app using Objective C. As a long time C++ programmer, I found Objective C very hard to grok, so I scurried back to the web.
Later, I made another simple app using Swift and UIKit. That was better. Swift was a nice, if not deceptively complex language. And I found UIKit using Swift verbose, but workable.
I figured SwiftUI would be the best of both worlds. And I thought it was something I could be productive in given my less than awesome UI skills. So I took some SwiftUI classes, did my research, and got started.
SwiftUI was an enormous paradigm shift for me. In the web world I never went reactive because I didn’t think the extra complexity was worth it. But I’d do most anything to avoid using Xcode’s Interface Builder. SwiftUI is just code. Yay code.
SwiftUI lives up to the hype, for me at least. Since I was not a skilled UIKit developer to begin with, I didn’t miss most of the things other developers complain about.
Not that SwiftUI doesn’t have its own problems. It has lots of disturbing compiler quirks and bewitching quixotic bugs that can only be fixed with ritual sacrifice. But once you get the hang of it, you can actually make a decent UI. And that was my goal.
There's a magic in having a UI update based on data changes. When it works, it is magical. But beyond the simple examples people love to show, making it work as an app grows more complex requires a much deeper understanding of SwiftUI.
Most of the SwiftUI plumbing is hidden behind nearly identical sounding property wrappers, which are a deeper level of magical Swiftisms themselves. When the UI doesn't update as expected, it can be a very frustrating experience to find out what's wrong and make it right. The spice must flow, but the data doesn't always.
The complexity of SwiftUI for me was learning how to put it all together in a complete application.
How do you handle logged-in mode vs logged-out mode? How do you handle being offline? How do you handle ads? How do you integrate in-app purchases? Where do you store data? How do you query, update, and delete data? How do you handle login/logout? How do you handle syncing between devices? How do you test in Xcode? How do you handle onboarding? How do you learn all the magical bits of Swift + SwiftUI syntax? How do you validate forms? How do you present errors? How do you integrate asynchronous services? How do you handle moving to and from the background? How do you handle push notifications? How do you navigate between views? How do you adapt to different screen sizes? How do you react to all these modes as they change within a reactive paradigm?
In the web world I know how to do all this. In SwiftUI, it took a long while before I could make it all work together without the crutch of procedural code or familiar processes.
My choices also helped to make the app more complicated. After using over a dozen different fitness apps, I grew to loathe the apps that immediately required you to login and pay up front for your “free” trial. So Max reHIT Workout works perfectly well without a login or an in-app purchase. That means handling all those modes. Not fun.
Conforming to the native iOS ecosystem means using frrameworks like in-app purchases, CloudKit, CoreData, and Apple Sign-in. Most of which I had not used before. So that was quite a learning curve.
There’s always a learning curve. There are always bugs. There’s always tooling problems. There are always problems with poor documentation and an almost complete lack of good example code. I don’t think there’s a way around any of those problems as a software developer. But you get to pick your poison.
I used Revenuecat to help with the payment processing. Is it simple? Not at all. Is it obvious how to integrate into a SwiftUI app? Not at all. Same with AdMob. That’s why everything takes so dang long to make work.
One big mistake I made was not using CloudKit and CoreData from the start. This was because my initial plan was to just make the app work on one device. I did the simplest that could possibly work and, as usual, I paid for it later.
I had used CoreData to store user data as well as UserDefaults. For a large dataset I was storing, I used a JSON file. This worked, but was complicated. Everything is complicated.
Later I decided I wanted Max reHIT Workout to work across devices. I stored the JSON file in the cloud using CloudKit. That worked, but it didn’t sync.
To sync, I’d have to use CoreData and CloudKit. I started slow and moved everything to use CoreData. Then I moved CoreData to use the PersistentCloudKitContainer. When you use SwiftUIs @FetchRequest in a view it just works. You could have knocked me over with a feather. As an extra plus it keeps a local cache, so it works offline and resyncs when the connection is restored. It all syncs, scales, is secure, and is completely free.
The problem is not all data access occurs within a view. And that's where SwiftUI falls down. It's view centric. All the neat tooling doesn't work outside of views. So you have to figure that out.
Of course, there are complications, and tricks to make it all work. I could usually figure it out by Googling or asking questions on Reddit.
Was using a native iOS ecosystem quicker and easier than web + AWS? Not at first. The learning curve was huge. My next app will be a much smoother experience.
I now have reusable code. A plus of Swift is making reusable components is encouraged by the language. You don't have to fight the environment. And since SwiftUI is built around components, you can also create reusable UI components—once you figure out the dark arts of binding and observable state. Personally I would stay away from protocols in SwiftUI. They caused more compiler problems than anything else.
The obvious downside is my app only works on iOS. Need a website or an Android app? Out of luck.
For this app, I thought that was a tradeoff worth making. This is the kind of paid app that iOS users might find valuable. Would they find it and use it on the web? Would they buy it on Android? That’s something you must decide on a property by property basis.
And of course if Apple decides they don’t like my app, I have no recourse. I don’t expect that to happen. My experience with app review has generally been positive, but you never know.
What about the 30% App Store charge? Who likes that? It will be 15% for me because I was finally accepted into the App Store Small Business Program. If I ever get above a million dollars a year in sales I’ll worry about that then.
I’m fine with 15%. The native in-app purchase and sign-in experience are so much better than PayPal and Cognito. Testing and debugging it in Xcode is nearly impossible. But PayPal integration is no day in the park either.
When you add in the cloud storage and syncing functionality, I think it’s worth Apple’s cut. But if you’re multi-platform and not leveraging iOS services, I can see how the cost would rankle.
Would I use a native iOS ecosystem again? Yes, for certain kinds of properties. Now that I know what I’m doing, I'm quite productive.
But if I needed to be multi-platform and I couldn’t afford a native team for each platform, I would choose a different path. One path would be to create different backend code generators for SwiftUI. I can see SwiftUI being used to create websites and possibly even Android apps.