A working iOS app in a day, and the parts AI didn't do for me

AI-assisted developmentiOSSwiftClaude Codesoftware architecture

Filet — a wallet for your files, open in any browser

I write integrations for a living. I had never written a line of Swift, never shipped a mobile app, and never touched the iOS toolchain. I wanted to answer a narrow question, honestly: when an experienced engineer drops into an unfamiliar stack with a capable AI assistant, what actually limits the work — the syntax I don’t know, or the judgment I do?

The only way to get a real answer was to ship something. So I did, in about nine hours of focused work inside a single day, and put it on GitHub.

The problem

The app came from a genuine annoyance: moving a 4 GB file onto my iPhone without a cable, an account, or uploading it to someone else’s cloud. The idea — the phone runs a small web server; any device on the same Wi-Fi opens a browser and transfers files; no app or account on the other end. I called it Filet: a wallet for your files, open in any browser.

That framing already contains the hard parts: streaming multi-gigabyte files at constant memory, an auth model for an HTTP service on a home LAN, and a UI on two surfaces (native control app + browser client) that has to feel like one product.

The method: the AI writes, I decide

I didn’t type Swift. Claude (Opus 4.8, via Claude Code) wrote essentially all of the code. My job was the part that doesn’t change when the language does: decide what to build, in what order, and how to know it works. A few practices did most of the heavy lifting.

Scope before code. I started with a structured scoping interview rather than a prompt. The output was a SCOPE.md with explicit decisions — auth model, collision policy, what’s v1 versus v2 — and a “verify, don’t assume” list of iOS behaviours we were not allowed to take on faith. That document anchored every later decision.

Benchmark before architecture. The first phase wasn’t a feature. It was a measurement: stream a 4 GB upload through two candidate servers and watch memory. FlyingFox held a ~7 MB peak against a 4 GB body; that result — not a preference — chose the library and dictated the shape of the streaming handler. The architecture stayed deliberately thin until that number existed.

Phases with gates. The build ran as eleven numbered phases — server lifecycle, auth, download, upload, folders, thumbnails, native browse, Files-app visibility, real-world test. Each phase had to be verified before the next began. No phase was declared done on a claim; it was done when I’d seen it work.

Verify, don’t assume. This is the discipline that mattered most, and it’s pure integration-engineering instinct. iOS APIs were tested for actual behaviour, not asserted. Does QLThumbnailGenerator produce a frame for an arbitrary video? Measure it. Does the response path really stream a 4 GB download at constant RAM? I downloaded 4 GB and watched the process: 111 MB peak. Does a >2 GB range request survive the 32-bit boundary? Fetch at offset 3,000,000,000 and compare bytes.

Tests for the logic that has edges. The pure logic — path-traversal resolution, the thumbnail cache’s eviction, session idle-timeout, the QR encode/decode round trip — was built test-first, in small red-green steps. These are Foundation-only types that compile and run without a simulator, so the core safety properties are checked in seconds.

Keeping the assistant sharp

Matt Pocock has pointed out that agents tend to degrade once they’re past roughly 40% of their context window — around 100k tokens. I took that seriously. Working one phase at a time, plus a terse “caveman” interaction mode that strips filler from the exchange, kept each phase comfortably under that 100k mark. Between phases I used a handoff step that compresses the state into a short document the next session reads cold — what’s done, what’s next, the gotchas — so each session starts with a small, high-signal context instead of dragging the full history along. It’s the same reason you write a good runbook: the next operator shouldn’t need the whole story to act correctly.

I also ran a second, stronger model as a reviewer at each design point and before declaring anything done. That was not ceremony. It caught real defects I would have shipped:

  • A claim that an actor serialised thumbnail generation and therefore bounded memory. It doesn’t — Swift actors are re-entrant, so the work runs concurrently. The fix was to measure peak RAM under concurrent load instead of asserting a bound that didn’t exist.
  • A build-config change that silently dropped UIFileSharingEnabled and reset the version string — invisible on the simulator, and exactly the kind of thing that only bites at device sideload.
  • A session idle-timeout I added late, which introduced a new failure path: a 401 after the session expired left the web client on a stale screen instead of returning to the pairing page. Caught and fixed before it ever ran on a device.

What the AI didn’t do for me

This is the honest part, and it’s where the answer to my original question lives.

The assistant could not decide that a benchmark should come before the architecture. It could not choose the auth model, or that “verify, don’t assume” should be a hard rule, or which assumptions were worth the paranoia. It could not tell that an empty share sheet on the simulator was a tooling limitation rather than a bug, or that a multi-select download had to be confirmed by hand because the browser gesture-gates it. It needed me to read its diffs critically, to insist on evidence, and to do the device-only verification — the real iPhone, the real 4 GB transfer, the things a simulator can’t prove.

None of that is Swift knowledge. It’s decomposition, interface and boundary thinking, knowing what to measure, and reading work skeptically. Those transfer intact from integration work. The framework-specific knowledge — the part I genuinely lacked — was the part the AI covered well.

Is this vibecoding?

Someone will say it, so I’ll answer it. The term, as Andrej Karpathy coined it, describes giving in to the vibes — you prompt, accept the diffs without really reading them, forget the code exists. The defining trait is not engaging with correctness: you take what the model gives you and move on.

By that definition, this isn’t that. The code was generated — I didn’t type Swift. But correctness wasn’t taken on faith at any point: every iOS API was tested for real behaviour, the core logic was built test-first, the streaming claims were measured against actual gigabytes, a second model reviewed each design decision, and the whole thing was verified on a physical device. I read the diffs, rejected some, and made the architectural calls.

So I won’t deny the AI wrote the code — it’s in the README — and I won’t pretend I hand-crafted Swift I’d never written. But “vibecoding” implies the correctness was left to chance, and it wasn’t. The honest label is AI-assisted, spec-driven engineering: the code was generated, the correctness was earned. That distinction — between accepting output and verifying it — is the whole job.

Where it landed

Filet works. It runs on a real iPhone, moves multi-gigabyte files over the LAN with PIN-and-approval auth, generates thumbnails, shows under the Files app, and shares through the native sheet. The native app and the browser client share one design system. It’s open source under MIT.

The native control screen — PIN, QR, and connect address while the server runs

Browsing files in the native app — extension chips and folder "pockets"

The design, incidentally, was also Claude’s — I described the brand (“a wallet for your files”) and it produced the palette, tokens, and mockups for both surfaces; I implemented to match.

The browser file manager — the main surface, open in any browser on the same Wi-Fi

What I take from it

I’m not going to claim AI makes everyone a mobile developer. What I will claim, because I now have one concrete data point, is narrower and more useful: for an experienced engineer, the unfamiliar stack is no longer the bottleneck. The bottleneck is the engineering judgment — scoping, sequencing, verification, reading a change critically — and that judgment is exactly what a senior integration developer already brings.

Given good AI assistants and the discipline to direct them, I can build and ship working software outside my home stack. This project is the evidence.


Built from scratch with Claude Opus 4.8 (Claude Code); designed with Claude’s design capability. Engineering practices — test-driven development, context handoffs, and architecture review — followed Matt Pocock’s skills. Source: github.com/vinothkumr/filet-file-wallet.

All posts