Puremac Macos Cleaner

作者 reason-machines2384a003145a無授權條款83 個星標收錄於 2026年10月8日更新於 2026年10月8日儲存庫3 個月前更新

Free open-source macOS cleaner built with SwiftUI — CleanMyMac alternative with zero telemetry, scheduled auto-cleaning, and Xcode/Homebrew/system cache cleanup.

AI 產生的概覽

介紹開源 SwiftUI macOS 清理工具 PureMac,涵蓋安裝、程式結構、清理類別與排程清理。

功能
此技能提供 PureMac 的參考文件,PureMac 是一款原生 SwiftUI macOS 清理應用程式,可清除系統垃圾、使用者快取、Xcode 衍生資料、Homebrew 快取、郵件附件以及 APFS 可清除空間。文件說明了透過 Homebrew、直接下載或使用 XcodeGen 與 xcodebuild 從原始碼建置的安裝方式,並介紹專案結構、清理類別定義、掃描與清理服務、排程自動清理以及可清除快照的處理。其中也包含用於新增類別、透過 AppStorage 鍵設定排程,以及建置與測試應用程式的 Swift 程式碼範例。
適用情境
當你需要安裝、建置、設定或參與 PureMac 開發時使用,也適合想了解其掃描、清理、排程與可清除空間功能如何實作的情況。它同樣適用於新增清理類別或設定排程清理。
執行需求
從原始碼建置需要 macOS 13.0+、Swift 5.9 和 Xcode 15+;產生專案需要 XcodeGen。安裝或安裝 XcodeGen 需使用 Homebrew。可清除空間操作使用 tmutil,可能需要管理員認證。此技能本身僅為說明文件,不附帶指令碼。

PureMac macOS Cleaner

Skill by ara.so — Daily 2026 Skills collection.

PureMac is a free, native SwiftUI macOS application that cleans system junk, user caches, Xcode derived data, Homebrew caches, mail attachments, and purgeable APFS space. It is a privacy-respecting, open-source alternative to CleanMyMac X with no telemetry, no subscriptions, and no network calls.


Install

Homebrew (recommended)

bash
brew tap momenbasel/tapbrew install --cask puremac

Direct Download

Download the latest .app from Releases, unzip, and drag to /Applications.

Build from Source

bash
brew install xcodegengit clone https://github.com/momenbasel/PureMac.gitcd PureMacxcodegen generatexcodebuild \  -project PureMac.xcodeproj \  -scheme PureMac \  -configuration Release \  -derivedDataPath build \  buildopen build/Build/Products/Release/PureMac.app

Requirements: macOS 13.0+, Swift 5.9, Xcode 15+.


Project Structure

PureMac/├── PureMac/│   ├── App/│   │   └── PureMacApp.swift          # App entry point│   ├── Views/│   │   ├── ContentView.swift         # Main window│   │   ├── ScanView.swift            # Smart scan UI│   │   ├── CategoryDetailView.swift  # Per-category drill-down│   │   └── SettingsView.swift        # Schedule & preferences│   ├── Models/│   │   ├── CleanCategory.swift       # Category definitions│   │   └── ScanResult.swift          # Scan result model│   ├── Services/│   │   ├── ScannerService.swift      # File scanning logic│   │   ├── CleanerService.swift      # Deletion logic│   │   ├── SchedulerService.swift    # Auto-clean scheduling│   │   └── PurgeableService.swift    # APFS purgeable space│   └── Utilities/│       └── FileSizeFormatter.swift├── project.yml                       # XcodeGen spec└── CONTRIBUTING.md

Core Concepts

Clean Categories

PureMac operates on named categories, each mapping to specific filesystem paths:

CategoryKey Paths
System Junk/Library/Caches, /Library/Logs, /tmp, ~/Library/Logs
User Cache~/Library/Caches, npm/pip/yarn/pnpm caches
Mail Attachments~/Library/Mail Downloads
Trash~/.Trash
Large & Old Files~/Downloads, ~/Documents, ~/Desktop (>100 MB or >1 year old)
Purgeable SpaceAPFS Time Machine snapshots via tmutil
Xcode JunkDerivedData, Archives, CoreSimulator/Caches
Homebrew Cache~/Library/Caches/Homebrew

Large & Old Files are never auto-selected — the user must explicitly choose items before cleaning.


Working with the Codebase

Adding a New Clean Category

  1. Define the category in CleanCategory.swift:
swift
// CleanCategory.swiftenum CleanCategory: String, CaseIterable, Identifiable {    case systemJunk       = "System Junk"    case userCache        = "User Cache"    case mailAttachments  = "Mail Attachments"    case trash            = "Trash"    case largeOldFiles    = "Large & Old Files"    case purgeableSpace   = "Purgeable Space"    case xcodeJunk        = "Xcode Junk"    case homebrewCache    = "Homebrew Cache"    // Add your new category here:    case gradleCache      = "Gradle Cache"
    var id: String { rawValue }
    var iconName: String {        switch self {        case .systemJunk:      return "trash.circle"        case .userCache:       return "internaldrive"        case .xcodeJunk:       return "hammer"        case .homebrewCache:   return "shippingbox"        case .gradleCache:     return "archivebox"   // new        default:               return "folder"        }    }}
  1. Add scanning logic in ScannerService.swift:
swift
// ScannerService.swiftfunc scanCategory(_ category: CleanCategory) async throws -> ScanResult {    switch category {    case .gradleCache:        return try await scanPaths([            FileManager.default.homeDirectoryForCurrentUser                .appendingPathComponent(".gradle/caches")        ])    // ...existing cases    default:        throw ScannerError.unsupportedCategory    }}
private func scanPaths(_ urls: [URL]) async throws -> ScanResult {    var files: [ScannedFile] = []    let fm = FileManager.default
    for url in urls {        guard fm.fileExists(atPath: url.path) else { continue }        let enumerator = fm.enumerator(            at: url,            includingPropertiesForKeys: [.fileSizeKey, .contentModificationDateKey],            options: [.skipsHiddenFiles]        )        while let fileURL = enumerator?.nextObject() as? URL {            let values = try fileURL.resourceValues(forKeys: [.fileSizeKey, .contentModificationDateKey])            let size = Int64(values.fileSize ?? 0)            let modified = values.contentModificationDate ?? Date.distantPast            files.append(ScannedFile(url: fileURL, size: size, modifiedDate: modified))        }    }
    let totalBytes = files.reduce(0) { $0 + $1.size }    return ScanResult(category: .gradleCache, files: files, totalBytes: totalBytes)}
  1. Add cleaning logic in CleanerService.swift:
swift
// CleanerService.swiftfunc clean(_ result: ScanResult, selectedFiles: Set<URL>? = nil) async throws -> Int64 {    let filesToDelete = selectedFiles.map { Array($0) } ?? result.files.map(\.url)    var bytesFreed: Int64 = 0    let fm = FileManager.default
    for url in filesToDelete {        do {            let attrs = try fm.attributesOfItem(atPath: url.path)            let size = attrs[.size] as? Int64 ?? 0            try fm.removeItem(at: url)            bytesFreed += size        } catch {            // Log but continue — don't abort on single-file failure            print("Failed to delete \(url.lastPathComponent): \(error.localizedDescription)")        }    }    return bytesFreed}

Scheduled Auto-Cleaning

Configure via Settings → Schedule tab. Intervals: hourly, 3h, 6h, 12h, daily, weekly, biweekly, monthly.

swift
// SchedulerService.swift — how scheduling is implementedimport UserNotifications
class SchedulerService: ObservableObject {    @AppStorage("schedulingEnabled")    var schedulingEnabled: Bool = false    @AppStorage("cleaningInterval")     var cleaningInterval: String = "daily"    @AppStorage("autoCleanAfterScan")   var autoCleanAfterScan: Bool = false    @AppStorage("autoPurgePurgeable")   var autoPurgePurgeable: Bool = false
    private var timer: Timer?
    func scheduleIfNeeded() {        timer?.invalidate()        guard schedulingEnabled else { return }        let interval = intervalSeconds(for: cleaningInterval)        timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in            Task { await self?.runScheduledClean() }        }    }
    private func intervalSeconds(for key: String) -> TimeInterval {        switch key {        case "hourly":    return 3_600        case "3h":        return 10_800        case "6h":        return 21_600        case "12h":       return 43_200        case "daily":     return 86_400        case "weekly":    return 604_800        case "biweekly":  return 1_209_600        case "monthly":   return 2_592_000        default:          return 86_400        }    }
    @MainActor    private func runScheduledClean() async {        let scanner = ScannerService()        let cleaner = CleanerService()        for category in CleanCategory.allCases where category != .largeOldFiles {            if let result = try? await scanner.scanCategory(category), autoCleanAfterScan {                _ = try? await cleaner.clean(result)            }        }        if autoPurgePurgeable {            try? await PurgeableService.shared.purge()        }    }}

Enable scheduling programmatically:

swift
let scheduler = SchedulerService()scheduler.cleaningInterval = "weekly"scheduler.autoCleanAfterScan = truescheduler.autoPurgePurgeable = falsescheduler.schedulingEnabled = truescheduler.scheduleIfNeeded()

Purgeable Space (APFS Snapshots)

PureMac uses tmutil to delete local Time Machine snapshots — this is the only operation requiring elevated privileges:

swift
// PurgeableService.swiftimport Foundation
class PurgeableService {    static let shared = PurgeableService()
    func listSnapshots() async throws -> [String] {        let output = try await shell("tmutil listlocalsnapshots /")        return output            .split(separator: "\n")            .map(String.init)            .filter { $0.hasPrefix("com.apple.TimeMachine") }    }
    func purge() async throws {        let snapshots = try await listSnapshots()        for snapshot in snapshots {            try await shell("tmutil deletelocalsnapshots \(snapshot)")        }    }
    @discardableResult    private func shell(_ command: String) async throws -> String {        try await withCheckedThrowingContinuation { continuation in            let task = Process()            task.launchPath = "/bin/bash"            task.arguments = ["-c", command]            let pipe = Pipe()            task.standardOutput = pipe            task.terminationHandler = { _ in                let data = pipe.fileHandleForReading.readDataToEndOfFile()                continuation.resume(returning: String(data: data, encoding: .utf8) ?? "")            }            do { try task.run() } catch { continuation.resume(throwing: error) }        }    }}

Xcode Cache Paths

swift
// Paths cleaned by the Xcode Junk categorylet home = FileManager.default.homeDirectoryForCurrentUser
let xcodePaths: [URL] = [    home.appendingPathComponent("Library/Developer/Xcode/DerivedData"),    home.appendingPathComponent("Library/Developer/Xcode/Archives"),    home.appendingPathComponent("Library/Developer/CoreSimulator/Caches"),]

Scan these and safely delete their contents without removing the directories themselves.


SwiftUI View Patterns

Scan Progress View

swift
// Example: triggering a scan from a SwiftUI viewstruct ScanView: View {    @StateObject private var scanner = ScannerService()    @State private var results: [ScanResult] = []    @State private var isScanning = false
    var body: some View {        VStack {            if isScanning {                ProgressView("Scanning…")            } else {                Button("Smart Scan") {                    Task { await runScan() }                }            }            List(results, id: \.category) { result in                CategoryRow(result: result)            }        }    }
    private func runScan() async {        isScanning = true        results = []        for category in CleanCategory.allCases {            if let result = try? await scanner.scanCategory(category) {                results.append(result)            }        }        isScanning = false    }}

File Inspector (Click-to-Inspect)

swift
// Show files before deletion — users can deselectstruct CategoryDetailView: View {    let result: ScanResult    @State private var selected: Set<URL> = []    @State private var cleaned = false
    var body: some View {        List(result.files, id: \.url, selection: $selected) { file in            HStack {                Image(systemName: "doc")                Text(file.url.lastPathComponent)                Spacer()                Text(ByteCountFormatter.string(fromByteCount: file.size, countStyle: .file))                    .foregroundStyle(.secondary)            }        }        .toolbar {            Button("Clean Selected") {                Task {                    let cleaner = CleanerService()                    _ = try? await cleaner.clean(result, selectedFiles: selected)                    cleaned = true                }            }            .disabled(selected.isEmpty)        }    }}

Configuration (AppStorage Keys)

All preferences are stored in UserDefaults via @AppStorage:

KeyTypeDefaultDescription
schedulingEnabledBoolfalseEnable scheduled cleaning
cleaningIntervalString"daily"Interval key (see above)
autoCleanAfterScanBoolfalseAuto-clean after scheduled scan
autoPurgePurgeableBoolfalseAuto-purge APFS snapshots

Read/write from anywhere:

swift
UserDefaults.standard.set(true,    forKey: "schedulingEnabled")UserDefaults.standard.set("weekly", forKey: "cleaningInterval")

Building & Testing

bash
# Generate Xcode project from project.ymlxcodegen generate
# Build Releasexcodebuild \  -project PureMac.xcodeproj \  -scheme PureMac \  -configuration Release \  -derivedDataPath build \  build
# Run testsxcodebuild test \  -project PureMac.xcodeproj \  -scheme PureMac \  -destination 'platform=macOS'
# Open built appopen build/Build/Products/Release/PureMac.app

Contributing

  1. Fork and clone the repo.
  2. Run xcodegen generate to create the .xcodeproj.
  3. Create a feature branch: git checkout -b feature/gradle-cache-cleaning
  4. Follow existing patterns in ScannerService / CleanerService.
  5. Never add network calls, analytics SDKs, or telemetry of any kind.
  6. Large & Old Files must never be auto-selected for deletion.
  7. Open a PR against main.

See CONTRIBUTING.md for full guidelines.


Troubleshooting

ProblemSolution
xcodegen: command not foundbrew install xcodegen
App blocked by GatekeeperThe release build is notarized; if building from source, run xattr -cr PureMac.app
Purgeable scan returns 0 bytesNo local Time Machine snapshots exist — this is normal if TM is off
Xcode paths not foundXcode has not been used yet or DerivedData was already cleared
tmutil requires passwordPurgeable purge may prompt for admin credentials — this is expected macOS behavior
Scheduled cleaning not triggeringEnsure the app is running (it is not a background daemon); check Settings → Schedule

Safety Guarantees

  • Never deletes system-critical files or application bundles.
  • Only removes caches, logs, temp files, and user-selected items.
  • Large & Old Files require explicit user selection before deletion.
  • Purgeable operations target only APFS Time Machine snapshots — not free space.
  • All filesystem operations use FileManager.removeItem(at:) — no rm -rf shell calls for regular cleaning.

來源與署名

來源:reason-machines/trending-skills位於skills/puremac-macos-cleaner提交2384a00

授權條款: 無授權條款

內容歸原作者所有。SourceWeft 從公開儲存庫中收錄這些內容。

檢舉或申請下架