Making Ollie: Creating a local iOS app in a time of React Native

Mahyar McDonald
Ollie
Published in
7 min readOct 28, 2023
But what’s the 4th hidden face we are missing in this trilemma pyramid?

This is the first in a series of blog posts about how the Ollie iOS app is made, architected and what decisions we made along the way to create it. This post will start with the context of our app and why we decided to make a native iOS app first. Later posts will go into lessons learned and decisions we have made, including spicy ones about why we are deciding to move away from SwiftUI going forward.

Ollie (formerly GoodOnes), if you don’t know, is an app that helps you organize your photos with a self-updating local AI model, privately, without uploading your photos to our backend.

As you sort your photos into 3 piles of favorites, keep and trash, the AI learns what you like and updates itself on the device. Many of us joined because we all have a lot of photos to sort, not enough time to sort them and want to get the best photos from our phone. We believe “the photo mess” is still fundamentally unsolved.

Why We Chose To Go Native

When we started about a year ago, going native was a controversial choice. Most people expected us to go in the React Native or Flutter direction, for speed of development, ease of hiring and cross-platform app delivery.

The typical startup react native ‘shopping’ app.

However, from a product perspective, we knew we had to solve for these trio of constraints:

  • Speed: We wanted an app that ran locally on the users phone, integrated very well with their local native photo library and performed quickly, because people have zero patience for waiting or inconvenience. Needing to upload large photo files on a slow mobile connection kills this, not to mention issues that come from using a cross platform library. Deep PhotoKit, CoreML & Vision integration is not a strong suit of cross platform toolkits without diving into native code anyway.
  • Privacy: We didn’t want to rely on a backend because photos are very personal and private. Most people are not comfortable uploading their photos to an unknown service, especially an AI one. On top of this, this is something we personally believe in as a company.
  • Cost: Photo storage and running AI training & inference is a very expensive business to be in, and if we relied on uploading photos our costs as a small startup would balloon considerably.

At Ollie, we always try to apply the combination of first-principles thinking with Chesterson’s fence. We evaluate every decision by its merits, but if we’re going against the standard practice, we want to understand why others went with that option and what makes us different.

Where we landed is that most non-game apps for people’s mobile devices are what we call ‘shopping apps’. They are basic client-server applications that don’t maintain much state on the client and rely on backends as the ultimate source of truth.

As a result, their client apps are not data model heavy and could be replaced with a well made web app for the most part. Even Amazon’s mobile app is mostly webviews under the hood. These apps tend to not stress the phone resources and it is fairly well understood how to make them. If you are a startup making a shopping or “banking” app using a cross platform toolkit like Flutter, React Native or being web only is an understandable choice.

For us though, because of the constraint trio above, we stress all subsystems of the phone, with many threads leveraging the GPU, Neural Engine, CPU, network, database and storage systems simultaneously, since there is no server to offload this to. Because we make a high load app, it ends up being a stress test on new multithreading & UI libraries as a result, which means we need to be more conscious on how we manage threads than the typical client-server mobile app. We also need to do it quickly to not eat the user’s battery life.

TL;DR: Ollie more resembles a local desktop app than a server client app as a result, which makes data state screw ups and phone stress far more significant. Other startup founders are often surprised we didn’t use something like react native to make our app, but now it should be obvious why we chose this route.

App Stack: Standard choices

Let’s start with the basics — those are pretty standard choices for a native app these days:

  • Pure SwiftUI app with a few exceptions around playing transparent videos.
  • Swift Concurrency as the default multithreading model, though this will get a big caveat in a second.
  • MVVM: Model, View, ViewModel
  • GRDB for database ops and state with a repository pattern
  • The standard SaaS backend support: Datadog for debug logs, Mixpanel for analytics, Sentry for error handling, PagerDuty for alerting, Linear for bug tracking, LaunchDarkly for feature flags. We’re thankful we live in the age these all come out-of-box 🙂

Here’s a couple of places where we somewhat deviated from the standard:

  • Dependency Injection: We created our own basic dependency injection system that is like @Environment but is checked at compile time vs. run time with our own @Inject property wrapper. In exchange for compile time checking and simplicity of implementation, we gave up view tree scoping which we feel was a good trade off at our current app size. In practice, most environment objects live at the top of the app view tree anyway.
  • Performance Infra: such as a leak detector, performance span monitoring, AI performance monitoring and so on. Sentry provides some of this, but we wanted something simple to start.
  • Bug Reporting: we used Instabug initially, but it had a lot of weird limits and a surprising cost, to the point where we replaced it with our own code in the end and get bug reports via email + attachments now. This also allowed us to copy thread state for user reports.

App Stack: Where it got gnarly

We wanted folks to be able to submit their best photos to Google Photos, so we added Google Photos support with the the GPhotos library. We even hired the library maintainer for a while for some improvements in the library and are planning to submit the improvements back to open source. Gphotos support although proved to be a big source of complexity in our app that consumed a lot of engineering time in the end.

We use pretty much all the bells and whistles of Apple’s advanced libraries

  • CoreML for running inference and model updates (more on this in a future spot)
  • Apple’s Vision framework for feature extraction
  • PhotoKit to access the user’s photo library and modifying local albums.

All of the above combined with Swift Concurrency really tested the limits of what is possible to keep stable within a SwiftUI app.

Like many, we tried Combine due to it being the major interface for state with SwiftUI and after using it a lot, we realized it wasn’t a good idea with it’s many footguns and thus moved away from using it.

Looking Back: Are we happy that we went native?

When we were just getting started, we got quite a few incredulous looks when we decided to go native. When we were hiring, it was 10x harder to find great iOS developers, and so it was tempting to go with a cross-platform framework. When people ask us “when will you be on Android”, our answer is 🤷.

And yet, every single roadblock we hit would have been worse if we stayed on a platform like react native or flutter, not to mention that we would not be able to achieve our “holy trio” without going native. At the end of the day, we would have had to do a full-rewrite pretty quickly, and either re-skill or rehire, neither of which is a great prospect for a pre product / market-fit company.

So the purpose of this first article was to essentially advocate for native development, for all the woes that come with it. If one developer or would be startup reads it and it saves them the agony we went through, it will have been worth it 🙂

This is part of a series of blog posts where we go into:

--

--