Submitted

“The binary is done. The rest is not up to me.”

App Store Connect shows “Waiting for Review.” A status badge, yellow, in a browser tab. Months of work, reduced to a queue position.

There is nothing left to fix. Nothing left to optimize. The app is as good as I can make it, and now someone at Apple will decide if it meets their guidelines. I made a cup of coffee.

What Will Change

Real users will find bugs that two months of personal testing did not. Someone will use the app differently than I imagined. The first user review will mean someone used the app long enough to form an opinion. That is not a small thing.

What Does Not Change

The ADR list keeps growing. The code still has rough edges. What comes next depends on what real users find.

The disk was full. The app works. That was the goal.

Lessons

The toolchain is harder than the language. Swift is learnable in a few weeks if you have a background in typed languages. Xcode, signing certificates, entitlements, the sandbox, provisioning profiles, notarization, App Store Connect: these take longer. The language is the easy part. The platform is the course.

The compiler is on your side. Swift 6 strict concurrency produces errors that feel hostile until you understand what they are saying. They are almost always correct. An error about actor isolation is not bureaucracy: it is the compiler preventing a data race that would show up as an intermittent crash in production. Treat compiler errors as information, not obstacles.

Apple’s documentation is good when it exists. When it does not exist, you read source headers and WWDC session transcripts from three years ago. The half-life of Apple documentation is roughly one major OS release. Performance requires understanding the platform. Web development abstracts the underlying system behind HTTP, JSON, and framework conventions. Native macOS does not. getattrlistbulk versus FileManager.enumerator. Dispatch queues and their assertions. The difference between RSS and virtual memory. These are not exotic topics. They are the platform. The POSIX scanner was nearly twice as fast as FileManager. That gap is invisible without measurement.

What I Would Do Differently

Start with the POSIX scanner. I built the FileManager scanner first because it was obvious. Switching later cost time. The gap justified the rewrite.

Add PerfLogWriter earlier. The performance logging infrastructure that helped find the main thread stalls was added too late. Measuring is faster than guessing. Instrument first.

Two things I would not change: localization from the start, and writing the ADRs.

Localizing after the fact is painful. Localizing as you go is just a discipline. The app shipped in 11 languages. That would not have happened as an afterthought.

The Architecture Decision Records are 41 documents covering choices that were not obvious at the time. During the App Store rejection debugging, I could look up exactly why NSWorkspace.openApplication was called the way it was, and what alternatives had been considered. That is faster than reading git blame and inferring intent.

On GitHub Copilot

I used it throughout the project.

For architecture decisions, less so. It does not know the project’s constraints. It suggests patterns that work in general. Whether those patterns fit here requires judgment that only comes from understanding the specific problem.

The crashes, the memory optimization, the POSIX scanner discovery: those required reading documentation, running Instruments, and understanding the platform. Copilot can help write the code once you know what to write. Knowing what to write is the harder part, and always has been.

The Number That Still Surprises Me

5.2 million nodes on a full disk scan. That is what a modern macOS installation actually contains. Files, directories, symlinks, all enumerated, all sized, all laid out in a treemap in about a minute (Debug build, M3 Pro). The order of magnitude is real.

When I started this project, I did not know if that was achievable. The answer required choosing the right syscall, understanding why FileManager was slow, and measuring everything.

The project by the numbers:

MetricValue
Commits~353
ADRs41
Languages11
Submission cycles3
Scan speedup (POSIX vs FileManager)~1.7x
Memory (before)1.2 GB
Memory (after)469 MB
Duplicate detection113 to 397 groups/s

The app is in the queue. The disk is no longer full. Sometimes the simplest problem statement produces the most interesting engineering.

References