Building a SwiftUI App in a Month
Last week I wrote about why I decided to build a Meal Planner app.
This week I wanted to share a few thoughts about how I went about building a SwiftUI app in a month. This isn’t going to be a technical article. I might get into some technicalities, but I wanted to focus on the trade-offs that I had to make, and the process that I employed to make those trade-offs.
My day job is to lead the Product Design team at Vend which involves working alongside my fellow-directors to improve the way we work to deliver our product to our customers. I’ll be doing a talk soon, alongside our Director of Product (Anne Dröge) and VP of Engineering (Cara Fonseca-Ensor), about what we’ve learnt throughout our time at Vend.
I wanted to employ those principles on this project.
What are those principles?
Here are a two maxims that stand out to me:
- Minimise the time spent, and maximise the customer’s time with value.
- Don’t optimise for not making mistakes, optimise for fixing them.
This means that we:
- Decide up front how much time we are willing to commit to achieving a goal. The team at Basecamp calls this appetite. We’ve found this term is hard to understand. Better to use something like budget. People understand a budget. We do it with money all the time—appetite is just a budget for time.
- Ruthlessly cut scope during that cycle to ensure we deliver something at the end of that time period. Again, in Shape Up the Basecamp team calls this scope hammering. At Vend, we talk about being super clear about the scope upfront, and we use MoSCoW to be explicit about that. Then we can cut the agreed upon scope during the cycle without much fuss.
- Set a maximum budget. At Vend we use 6 weeks. Intercom uses 6 weeks. So does Basecamp. This ensures we constantly ship things of value to customers which increases their time with value and minimise the changesets we deploy (smaller changesets means it is easier to find and fix mistakes1). This also means we don’t keep going until something is “right”. There’s always something else to do.
How does this apply to the app?
So I do not have a whole team of Engineers with CI-CD already set up. I did not know Swift (and had never built an iOS app before.) A whole lot of the SaaS best-practise wasn’t really going to help me here. I also had a fairly limited period of time to get this done. As I mentioned in the previous post, I had a month off from my day job because COVID-19 had put an abrupt halt to a trip to Europe and I needed to use the leave I had saved up for it.
So this is what I had:
- No experience. I didn’t know what I was doing.
- An upper-limit of 1 month.
- An idea for an app.
- An existing “alternative solution” that I needed to best (the Notion database)
I felt like I could apply these principles that I talked about at work a lot, to get something done. And if I couldn‘t—maybe we needed to rethink our principles.
My initial goal
The original plan for the app included:
- Write a SwiftUI app that ran on iPhone, iPad and iCloud.
- Use only Apple-technologies and infrastructure (like iCloud). I couldn’t learn Swift and SwiftUI and how to run web services. I needed to set some sensible limits. (This has an a additional side-benefit of being free.)
- The app would store recipes, let you create a meal plan and automatically create shopping lists for you.
- The app would let you share recipes (and maybe even your whole recipe book) with friends and your iCloud Family.
- The app would share your meal plans with your iCloud Family.
- The app would sync your recipes, plans and lists between all your devices.
- The app would let you automatically import recipes from a website.
That’s not a lot for 4 weeks. Is it? (It was.)
My approach
So I cheated on one point: learning Swift. I knew when this “4 week cycle” was going to start, and so I decided to learn Swift before the cycle started. This was a really big unknown that I needed to get to grips with pretty quickly—could I actually grok Swift, or would it take longer than I expected to get it.
I spent a couple of hours across a few evenings doing Swift Playgrounds (Learn to Code 1, and Learn to Code 2). I also started watching WWDC videos on Swift (and SwiftUI) instead of watching tv shows.
This meant that by the time I started on the cycle I already had some understanding of how I was going to put together the whole thing. In Shape Up, they call this a pitch. I had nothing written down, rather just vague ideas like “I’ll use iCloud and NSCloudKitPersistentContainer for this, and then use Family Sharing to let partners share their recipes with each other.”
I continuously set myself really short-term goals within a larger framework of “Recipes. Plans. Shopping List. iPad. Sharing…” which became less and less certain the further out we looked (I guess this is a Roadmap?). Step 1, make recipes work…so: “Build a screen to show a recipe where everything is hard-coded”. “Figure out how to set up CoreData with CloudKit and show a list of things”. “Set up the model for a recipe”. “Show a list of recipes”.
Ok, now “Plans”. Each of these larger blocks of work would be “just good enough” to enable me to work on the next large block. No extra time spent finessing and pushing pixels around. I needed to very quickly answer the next big unknown: can I string this all together to make something that actually kind of works.
I knew from the requirements that I need to support at least Recipes, Plans and Shopping List to have anything of any sort of value. These were Must-Haves. So I need to get these things mostly worked out and working or it wouldn’t matter how great my “Recipe View” was.
I also knew that there were some pretty big technical assumptions I needed to check. Can I rely on NSCloudKitPersisentContainer, or would I need to set up a web service? Again, if I waited too long to check these assumptions I could end up wasting a lot of time. I knew instinctively that the web service would be a lot of work (and completely blow muy budget of 4 weeks) but I didn’t know anything about NSCloudKitPersistentContainer.
There were other technical assumptions I could push back. I knew enough to understand that if I could get NSCloudKitPersistentContainer then there was some way to do sharing. Whether that would be using CKShare, or whether I’d need to migrate all the entries from private iCloud stores to the public iCloud store and build my own permissions layer. That I could figure out later. And again, if I didn’t have “Recipes. Plans. Shopping List.” then I didn’t have much use for Sharing. Later basket.
The circuit-breaker
You see, the key thing is that I needed to ship something by the end of 4 weeks. And if I spent time on anything that wasn’t absolutely neccesary to have something useful, I was wasting my time.
I also had 4 weeks off. Only 4 weeks. Then I’d go back to work, and I knew that if I didn’t have something working and in the App store, I’d drop the project. Creatio ex-nihilo (making something out of nothing) is much harder than evolving something over time2 if you are doing something part-time.
Those 4 weeks were also fixed. I was going back to work. I knew that there was Christmas in the middle of it. I knew that we were planning to go away for a couple of trips. And when, inevitably, other trips came up or were extended, they’d eat at the 4 weeks—I couldn’t give myself an extension. There was a circuit-breaker: my job.
The trade-offs
This means I had to make trade-offs. Every single person who I talk to about this project tells me:
- Oh, you know what would be great? Syncing this with my partner?
- Oh, you should make it so I can import recipes from a website!
- What about a cooking screen? Like big words, step-by-step?
To those customers and testers: Thank you. Seriously, thank you for being invested in my project. To product people reading this: I also know. I know those things. See that original plan? It‘s in there. But you have to cut things, or you won’t ever ship.
As I said, pretty quickly I punted on solving the sharing problem. I assumed iCloud Family Sharing would be integrated with NSCloudKitPersisentContainer in such a way that would make “sharing your database” trivial. I was wrong. I know I have to get there at some point—but I’m also confident there’s a solution to the problem in some form.
Two weeks into my 4-week cycle I dropped the automatic importing from scope. I had done investigation and determined that (1) there wasn’t an obvious solution and (2) this wasn’t an absolute requirement to make something of value. Our Notion prototype was valuable to us, we had been using it for years and it didn’t have this feature.
Three weeks into my 4-week cycle I had to cut Mac app from scope. I tried, and there were too many hurdles. Fluffing about with that wasn’t going to help me succeed at anything—it would ensure I shipped nothing.
A couple of days before I had to return to work I decided to drop iPad support in the first version. It was almost there—I didn’t have far to go, I could maybe choose to keep iPad support as I finished the app and maybe be a little late. But I dropped it. I could add it later, and it was more important to ship something, than risk shipping nothing.
I missed a couple of big unknowns in my original planning—which is why I needed to shed scope aggressively at the end. I had forgotten about Subscriptions. I had to decide whether I wanted to include Subscriptions or not near the end. Technically I didn’t need them, but I knew I would forfeit a really important piece of feedback if I didn’t ship with it: product-market-model-channel fit.
I had product-market fit with the prototype. As I said, we had shared the template with a number of people who loved the concept and used the prototype in their weekly planning. The thing I needed to learn was: were people willing to pay for this thing? How, and how much? I needed to test that people were actually getting value from the app to know whether I was succeeding or not. They got value because they kept the app, despite the cost.
Learning often
I shipped TestFlight builds to a group of testers everytime I made any kind of progress. (Thank you team!) Their feedback helped me to understand what things I just couldn’t punt on.
I really wanted to punt on solving the “SwiftUI doesn’t really understand the concept of a FirstResponder” problem. This is when you can programatically say “bring focus to this TextField when you load this view”. I ran into this problem while testing and it annoyed me.
I also saw that the solutions weren’t super great. One option was to not use the SwiftUI TextField and instead use a UIViewRepresentable instance to wrap a UIKit UITextField which did understand FirstResponder. Here I would be introducing UIKit into the code base which I knew wouldn’t work on macOS (there you’d have to use an AppKit NSTextField). Then I’d be stuck relying on APIs that were platform specific.
The other option felt worse: a third-party library developed by mobilinked called MbSwiftUIResponder. That library comes with this warning:
This package is a temporary solution for controlling the first responder before the official APIs issued by Apple. This solution depends on the run-time (UIKit and AppKit) view structure - the code analyzes the run-time views and adds additional controls to the UIKit and AppKit views. It may NOT work in the future. The method used for this solution may not be possible as the SwiftUI implementation may change.
Yea, those both sound terrible. Let’s punt. Except, literally every tester messaged me: “Hi, I’m really annoyed that when this screen comes up I have to tap into the TextField before I can type”. “And this screen.” “And here.” “Honestly, I can’t use this, it’s so frustrating.”
Can’t punt on that. I don’t have automatic importing of recipes so the manual data-entry needs to be as fluid as possible. I opted for the third-party library because it feels like at some point Apple must release a version of SwiftUI with FirstResponder support.
Sidenote: This library, while super cool, also meant I had to do weird hacks (like set time outs) to avoid broken animations. This does sometime have weird side-effects of its own. But! Temporary solution. Come on, Apple!
Did it work?
Yes. While I shipped the app almost 4 weeks late (on the 7 February rather than 14 Jan), I shipped 18 commits after the end of the cycle out of a total of 190 commits that went into the build that shipped. Those commits are all either bug fixes reported by testers after the 4-week cycle or the work to “prep” for the App Store.
The delay was mostly around “prepping for the App store”. I had decided I would ship with bugs (we all do, even if we don’t know what they are yet) but I couldn’t ship without meeting all the App store requirements (like screenshots) and to do that, I needed to deploy test data to deploy multiple simulators which looked an awful lot like the work required to build basic onboarding…so I did that.
But the app worked by the of the cycle on 14 Jan. I had people using it. It had shed a lot of scope compared to the original plan, but it existed. And it was better than our Notion prototype in the key ways we set out to improve:
- You can start a Meal Plan on any day, and set the number of days the plan lasts for. You can work on multiple meal plans at once.
- No manual clearing out of recipes. New plan every time!
- New shopping list for every plan, so no unticking.
- The shopping list now gives you the right quantity of ingredients you need (8 tomatoes, 3L of milk) rather than just the number of recipes using the ingredient.
- Shopping Lists can get Extras. No weird recipes.
Now what?
I just released version 1.1
which includes iPad support. It’s a week after 1.0
hit the App store. I gave myself this weekend to get it done and out.
I also added behavioural tracking (using Segment) so we can understand where people get stuck and why they might not be progressing through the app. (Check our Privacy Policy for what we track—I’m taking this pretty seriously.)
I chose to do iPad next because the iPhone version running on an M1 isn’t super great. I want to be able to run this app on an M1 because (1) we have one and (2) it’s easier to do manual data-entry on laptop with a real keyboard than on an iPhone or iPad (then leverage iCloud to sync that data over to an iPad or iPhone which might be better suited to planning or shopping).
Time with value, compare to the baseline
I ran into a problem where Analytics for iOS appears to crash on instantiation on an M1. I have submitted a ticket and shipped 1.1
anyway. Time with value: let people use it with an iPad today. Nobody could run the app on an M1 before, they can’t today, so those people must wait on the support ticket. Nobody could run the app on an iPad before, they can today, so why make them wait?
From here, I’ll keep rolling. Work my way through the list of “I knows”. Bit by bit. I’ll keep setting budgets, with circuit-breakers. I’ll keep shipping just enough to make it better than before. And I know this way we’ll keep making progress. The app will keep getting better. And I’ll limit the amount of time I spend in the wilderness trying to make things work that don’t matter right now.
You can find Gather Meal Planner on the App store, and you can see me talk about Product at UXDX APAC.
-
Since we’re talking about Vend, I need to be clear—we make liberal use of CI-CD alongside feature flagging to ensure that we’re constantly integrating changes from across the organisation into production. Our engineers ship a change to production every 7 minutes. We also ship the parts of work that are ready “mid-cycle”. A key question we ask in every project is “Can we integrate these parts any sooner?” to minimise risk. ↩
-
If you are in this footnote then I apologise. You probably recognised Creatio ex-nihilo and thought “What on earth?” I wanted to make a clever reference, and I like the idea of playing these two ideas off each other that after often seen in conflict with each other—but really aren’t. ↩