SweepLoom

Reclaiming Xcode and iOS Simulator disk safely

DerivedData, device support, simulator devices and runtimes, archives and backups all live under ~/Library, and they are not the same kind of thing. How SweepLoom's desktop Cleanup screen tells regenerable cache from native objects and from data you cannot get back.

Sergii Ziborov9 min read
  • #macos
  • #xcode
  • #ios-simulator
  • #disk

Every iOS developer eventually opens the macOS storage settings, sees "Developer" taking a frightening share of the disk, and goes looking in ~/Library/Developer. The usual advice is a one-liner that deletes several folders at once.

Some of those folders are pure cache. Some are objects owned by a tool that expects to manage them. Some are the only copy of something you will want during your next crash investigation. SweepLoom's desktop Cleanup tab treats them as three different things, and this post walks through each. Caches such as DerivedData live under Build & packages; simulators and runtimes have their own iOS & Xcode source; archives and backups are under Archives & large data.

Three kinds of "Developer" data

What Where Kind How SweepLoom removes it
DerivedData Library/Developer/Xcode/DerivedData Regenerable cache, listed per project Confirmed generated cleanup
Device support Library/Developer/Xcode/iOS DeviceSupport (and watchOS, tvOS) Regenerable cache, listed per OS version Confirmed generated cleanup
Xcode downloads Library/Developer/DVTDownloads Regenerable cache Confirmed generated cleanup
SwiftPM cache Library/Caches/org.swift.swiftpm Regenerable cache Confirmed generated cleanup
Simulator devices Library/Developer/CoreSimulator/Devices, plus XCTest, SwiftUI preview and Playground device sets Native objects, listed per device xcrun simctl delete, shut-down devices only
Leftover device folders, CoreSimulator caches Library/Developer/CoreSimulator Files no registered device owns Removed only after a fresh re-check
Simulator runtimes managed by simctl Native objects xcrun simctl runtime delete, deletable runtimes only
Release archives Library/Developer/Xcode/Archives User data: builds and dSYMs Move selected items to Trash
Device backups Library/Application Support/MobileSync/Backup User data: restore points Move selected items to Trash

Every row in the app shows its origin path or native object ID, its size, the file count when known, how it changed since the last scan, and the consequence of removing it, in words. The consequence is the important column.

DerivedData: cache, but listed per project

DerivedData is the easy case. It is indexes, intermediate build products and logs, all regenerated by the next build. The only cost of deleting it is time: the next build of that project is a clean one.

That cost is why SweepLoom lists DerivedData by project instead of as one 40 GB blob. You can clear the folders for the five apps you have not opened since spring and keep the one you are working on today warm.

A verdict on every row

Listing by project only helps if you know which projects are alive. So each DerivedData folder is matched back to the project that produced it, and that project's state becomes a verdict in the row:

  • Suggested when the project no longer exists, or has been untouched for a long time.
  • Keep when it is clearly active: uncommitted changes, or a recent commit (or, outside Git, a recent edit).
  • Review for everything in between, including when Git did not answer in time.

The verdict comes with its reason in plain words, such as "Project deleted — safe to remove" or "Active project (3 uncommitted changes on main) — keep". It is a recommendation for you to read, not a selection made for you.

Generated cleanup is still gated. Right before removal, SweepLoom checks each path against the live working directories, executables and command-line arguments of running processes, and revalidates the metadata it measured. If Xcode is building into a folder at that moment, that folder is not touched. And a scan that only finished partially never authorizes generated removal: if SweepLoom could not measure all of something, it will not delete it on the strength of a guess.

Device support: one folder per OS version

Each time you connect a device running an iOS version Xcode has not seen, it copies that version's symbols into iOS DeviceSupport. A few releases later you have a folder per point release, most of them for versions none of your devices run any more.

SweepLoom lists these per version, with sizes. The consequence it shows is the honest one for every cache: regenerable, but the download or preparation will be needed again. If you plug in a device on that version tomorrow, Xcode prepares it again.

Simulator devices: read the records, then ask simctl

Simulator devices are where the one-liners go wrong. A device is a directory under CoreSimulator, but it is also a record that simctl and Xcode track. Deleting the directory behind their back leaves a device that still shows up in Xcode and no longer works.

SweepLoom therefore reads and removes them differently. To list devices, it first reads each device's own device.plist (name, runtime, last boot) straight from disk, in the default set and in the XCTest clone, SwiftUI preview and Playground sets. That matters because simctl can take 40 to 80 seconds to answer, or time out, while CoreSimulator is booting or migrating devices; reading the records means rows appear immediately. Then it confirms state with the tool:

text
xcrun simctl list devices --json

Each device becomes a row with its name, runtime, state and data size. A folder whose record is marked deleted, or that simctl no longer lists, becomes a separate leftover row. To remove a device, SweepLoom only ever goes through simctl. Devices get verdicts too, built from what is actually installed on them: a running simulator is Keep; one holding an app from a project you are actively working on is Keep; one whose runtime is gone, or that has not been booted in a long time, or that only holds apps from deleted or stale projects, is Suggested. Only devices in the Shutdown state can be selected. When you confirm, SweepLoom lists the devices again before acting. If the device has disappeared, it reports that. If it has been booted since you looked, it refuses:

text
Device is running; shut it down explicitly first.

Only then does it run xcrun simctl delete <udid>, Apple's supported command for removing a simulator device and its data. Devices whose runtime is gone are offered as one bulk row, but SweepLoom never runs a blanket simctl delete unavailable. While a runtime is being remounted, simctl can briefly report every device on it as unavailable; on the machine this was written on, that was 29 live simulators and about 64 GB. So the bulk row deletes only the exact UDIDs it listed, only for shut-down devices whose runtime is truly absent from simctl runtime list, and it aborts if that count changed since you looked.

Occasionally a device folder is left on disk that simctl no longer knows about. SweepLoom will remove such a leftover only after re-checking that it is not registered with simctl, is not a symlink, and is not in use by a running process.

Runtimes: the biggest items, with the strictest checks

Simulator runtimes are often the largest single items on a developer Mac, several gigabytes per iOS version. They come from:

text
xcrun simctl runtime list --json

simctl reports whether each runtime is deletable. SweepLoom follows that flag exactly. A runtime that is not deletable is shown, with its size, and the row says Runtime is managed by Xcode; cannot remove. A deletable one says who depends on it: either that no simulators use it and when it was last used, or how many simulators use it and that removing it makes them unavailable.

Removal is refused while any simulator is running, not only one of that runtime, because removing a runtime under a live simulator is the kind of thing that ends with a restart. Stop the simulators, confirm, and SweepLoom runs xcrun simctl runtime delete <id>.

Next to the runtimes you will also see CoreSimulator's dyld shared caches, one folder per runtime build. The ones in your home folder are files SweepLoom can remove once no simulator is running. The system copies under /Library/Developer/CoreSimulator are root-owned, so they are listed read-only with a note: they go away with their runtime, or with xcrun simctl runtime dyld_shared_cache remove.

Archives and backups are not cache

This is the line most cleanup tools blur.

Xcode release archives contain the build you shipped and its dSYMs. Those dSYMs are how a crash report from a user's phone turns into a line number. They cannot be reliably regenerated: a rebuild of the same commit generally does not reproduce binaries whose UUIDs match the crash reports you already have. Device backups are restore points.

SweepLoom never treats either as generated. They appear in Archives & large data, with a consequence that says what you lose, and the only action is to move the items you selected to the Trash. Trash is restorable in Finder, which is the point. It also means the space is not free until you empty the Trash yourself, and the app tells you so rather than reporting the bytes as reclaimed.

Measuring honestly

Two details make the numbers trustworthy:

  • Sizes use allocated blocks, not apparent sizes. Simulator data is full of sparse files, whose apparent size can be far larger than what they occupy on disk. Summing apparent sizes is how a cleanup tool ends up promising far more space than it can free.
  • Unknown stays unknown. If a folder cannot be read, its size is shown as partial or unknown, never as an exact zero.

Measurements stream in the background, and keep running while you look at another tab or the app is hidden in the menu bar. Completed scans land in History → Disk scans, so next month you can see which folder grew back.

Run it as a signed app

macOS ties folder-access permissions to an app's code signature. A plain cargo run binary is ad-hoc signed by the linker, which identifies one exact binary hash, so every rebuild looks like a new app and macOS asks for Documents, Downloads or Desktop access again.

From a checkout, build the signed bundle instead:

text
python3 scripts/macos-app.py
open target/SweepLoom.app

The script builds, assembles and signs the whole bundle with a code-signing identity from your keychain, keeps the bundle identifier com.weavatrix.sweeploom stable, refuses an update that would not satisfy the installed app's designated requirement, and never falls back to an ad-hoc signature. Grant folder access once and it stays granted across compatible updates. The details are in docs/MACOS.md.

A sane order of operations

  1. Open Cleanup → Build & packages and clear DerivedData for projects you are not actively building. The Keep / Review / Suggested reason on each row tells you which those are.
  2. Clear device support folders for OS versions none of your devices run.
  3. Open Cleanup → iOS & Xcode, shut down what you are not using, and delete devices you do not need. Remove old runtimes last, with every simulator stopped.
  4. Look at archives and backups separately, as data rather than cache. Keep the archives for anything still in users' hands.
  5. Empty the Trash when you are sure. That last step is yours.