I'll be honest, I wasn't an iOS veteran engineer when I started work on Quick Reviews at the start of this year. I'm really proud of what I made, I'm happy with how quickly I was able to get it out there, and I'm always delighted to see someone post a review from it online, but it's gone through a rough patch recently in terms of stability and development.

After launch, one of the requests that kept coming up was iCloud sync across devices. I wanted it too, and while I didn't have any idea how to make that work, I wanted to make it happen for people. Maybe it speaks to how straightforward an app Quick Reviews is, but it turned out iCloud was the most complicated thing I had done yet in the app. Not only did it complicate the code, but it complicated the testing and deployment processes as well.

I'll just get straight to it and say my fundamental error was how I elected to store data at the start of development. My problems were twofold:

  1. I was storing reviews to UserDefaults
  2. I was storing them as a single JSON object

I can already hear experienced iOS developers screaming in terror, but it was easy to implement and it functionally achieved what I was trying to do, so happy days.

Then two things hit at once, my diving into iCloud and reports from users that the app was getting slow to perform certain actions, specifically anything that had them saving their review data. The performance issue was caused by the fact that this JSON object with all of your reviews was getting quite massive. Remember that base64 formatted images are in this data, so each review can easily take up 1MB or more of data in that object. Our phones are pretty fast these days, so they can parse JSON in the blink of an eye, but eventually things get a bit tough. People were using the app to write lots of reviews and they were using it to import their Letterboxd histories, some of which had upwards of 1,000 reviews in them. You can surely see where this is going, but every time I had to save a review, iOS had to open that JSON file, run some logic around duplicate checking and a few other minor things on a JSON object that could get into the hundreds of megabytes (the 309 reviews in my own app is 208MB), which even on the latest iPhones takes a few seconds. What this meant to users was they tapped "save" after writing a review and the app would lock up for 3-5 seconds (or maybe more in unreported cases) as it processed a few hundred megabytes of JSON.

Add to this the fact that in order to do iCloud sync, I could not use UserDefaults (at least not directly). And even if I did, I'm pretty sure it would have to upload and download the entire JSON object (again, possibly hundreds of megabytes) on every sync, which was super wasteful and slow.

Both of these made it clear that I needed to switch my data to be stored using SwiftData, which is what I should have been using since the start. Not only would this radically improve my performance since there would no longer be a single, massive blob of review data, it would also turn the data into something that works well with iCloud. Two birds, one stone, right?

Right???

Kinda, actually, but this also introduced a whole bunch of other problems for me. Changing the data model in pre-production is relatively painless since you don't have any users and they've got nothing to lose. I did have users, though, so I needed to figure out how to migrate their reviews from one format to another. This is a scary and slightly complicated thing, and it took me a while to set this migration tool up, but after weeks of work I got something that worked in all of my internal testing. I deployed it to my TestFlight users and they did a bunch of migrations, all of which seemed to work (or if they didn't, they didn't say anything). Skipping ahead for a second, I did ship this new data model in the 1.1 update, and while it seems most users made it through okay, a few users did experience data loss, and I still feel really bad about that. I know Quick Reviews is a fun little app and the data is not that important in the grand scheme of things, but it felt bad all the same.

But it wasn't just the migration that made things more complicated. I also had tons of logic built into the app that assumed data was being stored a certain way, and changing the data model meant having to change all that logic as well. Without getting into the code specifics here, let's just say there were dozens of things I was doing with cached data, deduplication of reviews, and other tiny things here and there that needed to be changed to work with the new data model. I found most of them before that 1.1 release, but I missed some that users told me about and I had to push out a few quick updates after 1.1 to get the app into a more stable state (1.1.3 is imminent with a fix to review duplication bugs).

And finally, there was the iCloud sync itself. So on top of the data migration and tons of updated logic, I was also pushing and pulling updates to and from the cloud. Again, without getting into the hairy details too much, an example of an issue I was having was that I would delete a local review, it would get deleted from iCloud, but then another device that still had that deleted review would open the app, see that this review was missing from the cloud, and push it up…effectively, I made it impossible to delete anything. Obviously this can be solved, and I eventually did, but there were tons of issues like this that simply weren't problems before. It turns out that yeah, sync is indeed one of the hardest problems in computer science.

As of today, I no longer plan on bringing iCloud sync to Quick Reviews

This is a real bummer because like I said, I want this too, but I think it's the right thing to make the app as good as possible. All of this development pain has just not been worth it since the app's largely stood still from a feature perspective for over a month now.

To be clear, I think the data model had to change, otherwise the performance would have just gotten worse and worse over time, but adding cloud sync on top of that is just too much for me at my current skill level. I know that people will be sad that their reviews don't sync, but honestly they're not going to care about syncing between devices if the core functions don't work like they should and they don’t enjoy using the app in the first place.

And yes, this is no one's fault but my own. I made a fundamental error when starting work on the app, and while my scale isn't huge by any means, it's a classic example of technical decisions working when you're small, but falling apart at scale. In retrospect, it's terribly obvious what I should have done and I'd do things differently if I rewrote the app from scratch. Hell, maybe if I had used this data model from the start, iCloud would be much easier to just flip on and would "just work".

I'm trying to iron out these last few bugs I know are still in there and then move onto adding a few other things I know people want (more media types for magic mode, for example). I could even spend some more time cleaning up corners of the UI that could make the app nicer to use as well. I would have spent the last 6 weeks doing those if I hadn't been stuck on this iCloud mess instead. I know a rewrite is usually a bad idea, but as such a young iOS developer (by experience, not age 😅), I do feel like I've learned so much since starting this project that I could do an even better job if I started from scratch…but let's not get ahead of ourselves.

All of this is to say thank you to everyone who has used the app and continues to post with it every day (I see you! ❤️), and I hope the issues haven't impacted you too much. I hope the new things I can add now that I'm out of the pit are welcomed, and I hope this article is a useful piece of real transparency that helps others understand the challenges of development. It's worth it, but it's not easy.