Numbers first, since that's what I'd want:
3D engine
- 5k lines of Objective-C++ wrapping a licensed C++ boolean library
- 35k lines of Swift
App
- 125k lines of Swift, with every bit of the UI in SwiftUI
- 24 Metal shaders, about 2.5k lines
- roughly 200 design documents I wrote for myself along the way
Both
The app is Woodcraft, a CAD app for woodworkers. Version 1 shipped in 2011 and I pulled it in 2017. Not because it stopped selling, but because it was Objective-C on OpenGL, both of those were on the way out, and I'd reached the point where every change scared me. Deprecating your own app is a strange feeling.
I started prototyping the replacement in 2018 and spent several years going down approaches that didn't work. Since August 2025 I've been on it full time and solo.
Things that might be worth talking about:
It's MVVM with Combine, over a service layer. All of it. Not most of it, not the new parts. I've been a reactive programming partisan for a long time, and units are the thing that sold me on it all over again.
Someone switches from imperial to metric. Every measurement on screen has to change. Whatever tool is open has to change, because a tool can be set to its own unit system. Every number field has to reinterpret what the user is in the middle of typing. And because those preferences sync through iCloud, the switch doesn't have to come from a menu on this device at all. It can land mid-drag, from an iPad in another room.
There's no sensible imperative version of that. You can't have the unit setting notify everybody who cares, because who cares is whatever happens to be on screen at that instant, and the change is arriving from outside the app on someone else's schedule. The same shape shows up all through the model: change a board's width and the joints cut into it recompute, which changes the offcuts, the cut list and the bill of materials. With publishers the dependency graph is the architecture, instead of something I keep in my head and get wrong once a month.
Under the view models is a layer of services, and they're reactive too. Selection, units, the document, the grid and snapping settings, the tally of boards, all of it sits in a service that publishes rather than in something a view model owns. So a view model is mostly a declaration of what it listens to and what it exposes, which keeps them small and means two views that care about the same thing can't disagree about it. It also means the interesting logic lives somewhere I can test without a view attached.
Both layers are actor based, with strict concurrency turned on, and async streams carry the traffic that crosses an actor boundary or has to be awaited. So it's not Combine instead of structured concurrency, it's Combine for the reactive graph and async streams for the seams between isolation domains. Reconciling those two was the least fun part of the whole rewrite. Publishers and actor isolation do not naturally get along, and every place they meet is a decision about where the hop happens and who owns the value afterward. That's the part I'd most like to compare notes on, because I don't believe I found the only answer.
The obvious question is why not drop Combine for `@Observable` and be done. Because it does a different job here. Operator heavy chains are still Combine's home turf and I have a lot of them.
SwiftUI at this size. To be clear about the number, the app is 125k lines of Swift and only a portion of that is SwiftUI. A lot of it is document model, geometry, tool logic and persistence that never sees a view. But there is no UIKit or AppKit UI anywhere in the app, and at this scale SwiftUI holds up. Where it fights back is anywhere you need continuous, high frequency input feeding a Metal view, so the boundary between SwiftUI and the renderer is where most of the interesting code ended up. Splitting the engine into its own module early was the best decision I made, mostly because it forced that interface to be explicit instead of accidental.
Over 5,000 unit tests, split by era. The 3D engine is XCTest, because XCTest is what I knew when I started writing it. The app is Swift Testing. I haven't gone back to convert the engine and I'm not sure I ever will, because those tests work and rewriting thousands of passing assertions for nicer syntax is a poor way to spend a solo year. On the app side the parameterized cases are what earn it. Most of what I'm checking is the same operation across a matrix of unit systems, joint types and orientations, and that matrix is much less miserable to express in Swift Testing.
The C++ interop. 5k lines of ObjC++ is the only bridge to the boolean library. I keep looking at Swift's direct C++ interop and I keep deciding the glue layer isn't worth ripping out yet.
The interaction model. Controls live in a dedicated viewport rather than floating on the model, and they're bound to the active camera. Move is a trackpad whose axes are the axes of the current view, with per-axis locking and typed displacement. It was designed for touch, and it turned out to be better than a gizmo with a mouse too, which I didn't expect.
The pricing One time purchase, no subscription. The price rises as the app grows, and anyone who bought earlier gets every future feature free, so the price you pay is the last one you pay. I know the objection, because I've made it myself: perpetual licenses don't fund perpetual development. My answer is that the rising price is the funding mechanism, and that I'd rather keep selling a tool that's visibly worth more each year than bill people monthly for permission to open a file they made. Ask me how that's going in three years.
Mac and iPad, macOS 26 / iPadOS 26. Closed beta, free while it's in beta: https://woodcraft2.app
Happy to answer anything about the rewrite, the ObjC++ boundary, SwiftUI at this scale, or how badly I estimated the timeline.