DEV Community

Simple Memo
Simple Memo

Posted on

I shipped an iOS app with zero third-party dependencies

At 11:47 PM on a Sunday, I deleted the last import Alamofire from my Xcode project. I replaced eight network calls with seventeen lines of URLSession code. The unit tests passed. The app cold-started about 80 milliseconds faster. I went to bed.

That was the moment I committed to a rule I have not broken since: the iOS app I'm shipping carries zero third-party dependencies. Twelve months later, it is the single most contentious thing about how I work as a solo dev.

TL;DR

  • Twelve months of zero third-party dependencies on a small iOS app got my IPA to about 2.1 MB and my cold start to 280 ms median, in exchange for roughly two weekends of work I would have skipped with off-the-shelf libraries.
  • The biggest win is not performance. It is that every line of Swift in the project is debuggable by me, with no source-map archaeology and no vendored module to apologize for at 1 AM.
  • The rule is not absolute. I would add a third-party library tomorrow if it solved a problem I could not reasonably solve myself in one weekend, with tests, and with a maintainer who actually answers issues.

The setup

I am a solo developer. I ship an iOS app called Captio-style Simple Memo. It does one thing: you type a note, you tap a button, the note is in your email inbox in roughly 0.3 seconds. No accounts. No sync. No cloud database. The whole product is a thin UI on top of MFMailComposeViewController, a small encryption layer for the local draft cache, and a handful of system frameworks.

The first prototype, in late 2024, used three third-party Swift packages: SnapKit for layout, KeychainAccess for secure storage, and a small async HTTP wrapper around URLSession I had cargo-culted from my last job. None of them were strictly necessary. All three were habits.

By month two, two things had happened. The IPA had drifted past 6 MB. Cold-start times measured on my old iPhone 12 mini moved from 220 ms to 410 ms. Neither number is ruinous on paper. But for an app whose entire identity is "you tap, the email goes," half a second of warmup is the product. So I started cutting.

What I tried first (and why it failed)

The obvious play was triage. Keep the libraries that earned their weight, drop the rest. I made a spreadsheet. Each row: a third-party dependency. Each column: bytes added, build-time added, time-to-replace estimated in hours, unique value over Apple's SDK.

It looked rational. It also failed. The triage spreadsheet got me to drop SnapKit, which I should have dropped on day one. It got me to keep KeychainAccess, because the time-to-replace estimate was four hours and I told myself four hours was too much. Three months later I needed to support an experimental share target that KeychainAccess did not handle cleanly in my setup. I burned six hours wrestling with the library before I gave up and wrote 60 lines of SecItemAdd / SecItemCopyMatching directly. The wrapper had saved me four hours up front and cost me six on the day I needed it most.

That was the moment the rule stopped being aesthetic. I was not removing dependencies because zero-dependency code is morally superior. I was removing them because every dependency I keep is a future morning I do not control.

The thing that actually worked

I rewrote the rule. Instead of "fewest dependencies possible," I wrote this in my project README:

A third-party dependency is allowed only if it would take me longer than one weekend to write a correct, well-tested replacement, AND the dependency is actively maintained, AND it has fewer than three transitive dependencies of its own, AND it ships with tests I can read.

The change is subtle but important. The old rule was a bias. The new rule is a gate. It forces me to do a tiny piece of estimation up front. It also forces me to confront an uncomfortable fact: most of the third-party libraries I had reached for in the past were not in the "longer than one weekend" bucket. They were in the "I never bothered to learn the system API" bucket.

Here is what fell out the other side of that gate, in twelve months of shipping:

  • Networking: 38 lines of URLSession plus a tiny Result-based wrapper. Replaced Alamofire and one custom in-house wrapper.
  • Keychain access: 62 lines wrapping SecItem*. Replaced KeychainAccess.
  • Layout: native Auto Layout with a small set of NSLayoutConstraint helper extensions, about 40 lines. Replaced SnapKit.
  • JSON: Codable. There was never a real reason to keep SwiftyJSON.
  • Logging: OSLog. There was never a reason to keep CocoaLumberjack.
  • Crash reporting: I do not have third-party crash reporting. I have MetricKit, a handful of TestFlight users who actually report issues, and a hard rule that any crash on launch is a release blocker.
  • Encryption for the local draft cache: AES-GCM via CryptoKit, plus about 90 lines of file-format glue I wrote myself. Replaced a small encryption helper library.

Total third-party packages in the shipping app today: zero. Total lines of code I wrote that exist solely to replace those libraries: roughly 300. Total time spent writing those 300 lines, including tests and rewrites: about 18 hours across twelve months. Two weekends, conservatively.

The numbers I care about, measured on a fresh install on an iPhone 12 mini:

  • IPA size: 2.1 MB
  • Cold start (icon tap to first usable frame): 280 ms median over 50 launches
  • Time-to-email-sent from first tap: median 312 ms
  • Build time, clean: 11 seconds
  • SwiftPM resolve time: 0 seconds, because there is nothing to resolve

I am not pretending these numbers are amazing. For an app that does one thing, they are roughly the floor. The point is that the floor was reachable, by one person, in evenings, without a heroic engineering budget, and without any meaningful sacrifice in code quality on the parts I actually care about. Most of that was simply the absence of work I would have otherwise been doing: no integration glue, no version-pin debates with myself, and no "this library moved to v3, here are the breaking changes" weekends I had not budgeted for.

Why does this matter for a small app?

There is a quieter reason I kept the rule. Every Swift Package Manager dependency I add is a small bet on someone else's calendar. If the maintainer of a 3,000-star library posts a deprecation notice next month, that is now my problem. As a solo dev I do not have the bandwidth to absorb other people's roadmap decisions on top of Apple's. Apple already gives me one platform whose roadmap I do not control. Adding a second is a tax I cannot afford to pay quietly.

There is also a debugging argument that I underrated until I lived it. When an iOS 17.x point release changed the behavior of MFMailComposeViewController on dismissal in a way that left my view controller stack in a strange state, I needed about 40 minutes with the debugger to figure out what had changed. If that flow had been mediated by a third-party "mailer" library, my best case would have been waiting for the maintainer to ship a fix, and my worst case would have been forking, patching, vendoring, and learning a codebase I had spent months pretending I did not need to read.

Zero dependencies, in this framing, is a debuggability decision. Every stack frame is mine. Every breakpoint lands in code I wrote and remember. The first time I hit a real production weirdness and the trace was entirely my own code, I noticed how much faster my brain moved. There was no library to be intimidated by.

What I'd do differently next time (counter-take)

I want to be honest about where this approach is wrong, because I have read enough "zero dependencies" essays to know they tend to be smug.

Three places I was wrong:

First, I was wrong about Sentry-style crash reporting. For a year I leaned on "if it crashes on launch, my few beta testers will tell me." That is fine at a few hundred users. It is not fine if I ever cross a few thousand. I will add a thin crash-reporting integration before any real marketing push, and I will accept that the "zero" in my rule becomes "one" the moment the product has scale.

Second, I was wrong to extend the rule to my server tooling. I run a tiny landing page on a small VPS. I spent a weekend writing my own static-site generator because of the rule. The site is fine. The weekend was wasted. The right answer was Hugo. The rule, I now think, applies cleanly to the shipping app and is mostly noise everywhere else.

Third, I underestimated the cost of "I'll write it myself" in the analytics layer. I have no third-party analytics. I have a single anonymous keepalive ping per app launch and a self-hosted PostHog instance I never finished setting up. I should either commit to running PostHog properly or pick a privacy-friendly hosted analytics product. Pretending the problem does not exist is not a strategy.

The honest version of the rule, after twelve months, is closer to: zero third-party dependencies inside the binary the user installs, while remaining boring and pragmatic about everything outside it.

Takeaways for other solo devs

If you are considering this for your own project, here is what I would do:

  • Run the gate at the moment you reach for any library, not after you have already typed import. The decision is cheap before it is in your code and expensive afterward.
  • Time-box the replacement. If your "I could write this myself" estimate is over one weekend, the library probably earns its place.
  • Write a one-paragraph block in your project README that names every dependency and the reason it survived the gate. Future-you needs the receipts.
  • Measure binary size and cold start before and after each library decision. Numbers protect you from your own taste.
  • Do not extend the rule to tools that never touch the user's device. Static-site generators, build scripts, and CI runners are not part of the product.

Open question for the comments

I am curious what other solo devs do here. Have you tried a zero-dependency rule on a shipping app, and did it survive contact with a real feature? Or did you have the opposite experience, a single well-chosen library that paid for itself ten times over?

What was the last third-party dependency you removed, and what did you replace it with?


I'm a solo dev building Captio-style Simple Memo, an iOS app that emails the note you just typed in about 0.3 seconds. I write here every few days about the messy parts of shipping things alone.

Top comments (0)