should have just rewritten it
Draft is a macOS menubar app. You hit a hotkey, say what you’re thinking, and it writes a polished message in your voice. Not a transcription of what you said. What you meant, written the way you’d write it. It runs a local Qwen 3.5-4B model on Apple Silicon. No cloud. It learns your style through an RLHF loop. Just me using it right now.
It had a crash bug. After about 10 hours of use, the app would just die. No warning, no graceful exit. Dead.
All three crash reports pointed to the same place: a null isa pointer dereference in _ButtonGesture.internalBody.getter. The root cause was SwiftUI’s AttributeGraph. Every time an NSHostingView went through its lifecycle, it left behind dangling pointers. Use the app long enough and one of those pointers gets dereferenced. Crash.
I knew this was a SwiftUI problem weeks ago. I knew menubar apps and overlay windows are where SwiftUI gets flaky. I knew it because I’d been patching around it.
Here’s what I built instead of fixing the real problem: periodic hosting view recreation every 50 sessions. AttributeGraph teardown observers on sleep, wake, resign, and screen change events. Toast hosting view reuse to avoid creating new views. Band-aids on top of band-aids. Each one bought me a little more time before the crash. None of them fixed the crash.
Today I did the thing I should have done from the start. I rewrote the entire overlay and menubar UI from SwiftUI to pure AppKit.
45 files touched. 3,950 lines added, 2,483 deleted. 21 new AppKit views created. 8 SwiftUI files deleted. The net result is about 740 fewer lines of SwiftUI in the codebase. 168 tests pass unchanged. The crash is gone.
The rewrite took one day. The workarounds took weeks. And the workarounds didn’t even work. They just pushed the crash window from 4 hours to 10 hours.
This is the pattern I keep falling into. Something breaks. I patch it. The patch holds for a while. Then it breaks again and I patch the patch. At some point the patches become their own system with their own bugs. And the real fix was always the same: just do the hard thing.
SwiftUI is great for a lot of things. It is not great for long-running menubar apps with overlay windows. NSHostingView in those contexts accumulates lifecycle debris that SwiftUI has no mechanism to clean up. AppKit doesn’t have this problem because AppKit doesn’t have an AttributeGraph. You manage the views yourself. It’s more code up front. But the code does what you tell it to do and nothing else.
Red bar: knew the real fix for weeks. Kept patching instead of rewriting. Should have just done it.