TmpDisk was an app I built back in 2011 to solve an issue I had managing multiple RAM disks that helped with Grunt buildfile speed and cleanup. Turns out I wasn’t the only one facing issues and TmpDisk took off, being featured in multiple magazine articles and spread across the web getting hundreds of thousands of downloads.
Since then the app has been essentially stable with no major feature requests or stability issues prompting the code-base to slowly deteriorate to the point it couldn't even be built on a modern MacOS system. This post is a (mis)adventure in modernizing an ObjectiveC app in Swift.
The target OS
Every MacOS app needs to be targeted at a minimum OS version in order to take advantage of newer language and framework features. Pulling data on MacOS version marketshare was actually very hard, and I couldn’t find a reliable source of truth for the ecosystem in general. Looking at the individual (anonymized) TmpDisk usage stats, you can see a very varied spread of OS versions.
10.12 for example is MacOS Sierra - released in 2016 so an almost 6 year old OS at this point. If I wanted to support those users with the new version then almost all the new Swift features like SwiftUI were off the table.
For the most part, the migration was a 1:1 code functionality replacement. That didn’t mean we couldn’t take advantage of taking 10+ years of coding experience to refactor the app. I simplified the StatusBarController, moved away from global notifications to blocks and cleaned up the UI and added some extra functionality to the autocrat manager and got to add TmpFS support - which was what prompted the entire rewrite in the first place.
The breaking changes
The first breaking change was having to migrate from the old Sparkle framework to the new one. Since I no longer had the old developer account with Apple, the migration from DSA to EdDSA had to be done with an interim version supporting both signing methods. This wasn’t terrible but I forgot to re-embed the DSA public key in the binary since the new instructions only show the EdDSA workflow and I had to quickly roll a new version. Testing the Sparkle upgrade was painful since each test required a full binary deploy to production one Github and the sparkle server.
Quick plug, if you’re looking for a server to help manage your AppCast.xml and/or anonymously profile your App users, check out Sparkle Server - written in Rust with a small footprint and simplified admin management.
The second breaking change was how to get TmpDisk to launch on login. The previous code to manually inject the app into the LoginItems array was deprecated and the new approach is to create a specific Launcher App. I ended up rolling this myself but if you run into the same issue LaunchAtLogin has a ton of details and a package to help you with a launcher.
NSArray *loginItemsArray = (NSArray *)CFBridgingRelease(LSSharedFileListCopySnapshot(loginItems, &seedValue));
So TmpDisk was working again, I had some basic UnitTests and had manually tested every app codepath. We were ready to release version 2.0.0 and enjoy the new codebase and functionality. What could go wrong?
Well apparently, testing against older MacOS versions.
In general for OS compatibility, I rely on the compiler to give me warnings. If the code compiles and runs against a modern system and we’re not using any deprecated frameworks or functions you’d expect the code to run “as is”… right?
Turns out that was a very wrong expectation. Almost immediately a 2.0 bug was filed with Windows disappearing on open on MacOS 10.12. Now this was quite a painful bug to track down, was the window crashing, was any functionality working? Finding a copy of 10.12 to run in a VM to test was almost impossible without downloading seedy vm’s from the internet. Never the less, with a help of an avid user, I was able to track down the problem and it was a doozy.
NSWindow Lifecycle Changes (Updated since Seed 2) If your application is linked on macOS 10.13 SDK or later, NSWindows that are ordered-in will be strongly referenced by AppKit, until they are explicitly ordered-out or closed (not including hide or minimize operations). This means that, generally, an on-screen window will not be deallocated (and close/order-out as a side-effect of deallocation).
What that meant is that in 10.13+ my code ran perfectly, the windows would initialize and get retained by the OS automagically. But on 10.12 the window would be immediately garbage collected. This meant I had to add a specific retain cycle and manual memory management to the app. A simple fix and we were ready to push a bug free🤞2.0.2 version of TmpDisk into the wild.
So ends about 15 hours of work to migrate the app and deliver a modern, Swift based TmpDisk to the public. Interestingly performance of the old and new app is pretty much the same and negligible in day to day usage. Given the embedded Swift binary the App size is quite a bit larger but at 27mb instead of 4mb it’s hardly an issue compared to the much improved code readability and access to modern OS features.
If you’re interested in a helper util for managing RAM disks for your Mac, feel free to take a look at TmpDisk - easily managing your RAM disks on MacOS