puremac-macos-cleaner
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePureMac macOS Cleaner
PureMac macOS清理工具
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.
来自ara.so的Skill——Daily 2026 Skills合集。
PureMac是一款免费的原生SwiftUI macOS应用,可清理系统垃圾、用户缓存、Xcode衍生数据、Homebrew缓存、邮件附件以及可释放的APFS空间。它是CleanMyMac X的隐私友好型开源替代方案,无遥测、无订阅、无网络请求。
Install
安装
Homebrew (recommended)
Homebrew(推荐)
bash
brew tap momenbasel/tap
brew install --cask puremacbash
brew tap momenbasel/tap
brew install --cask puremacDirect Download
直接下载
Build from Source
从源码构建
bash
brew install xcodegen
git clone https://github.com/momenbasel/PureMac.git
cd PureMac
xcodegen generate
xcodebuild \
-project PureMac.xcodeproj \
-scheme PureMac \
-configuration Release \
-derivedDataPath build \
build
open build/Build/Products/Release/PureMac.appRequirements: macOS 13.0+, Swift 5.9, Xcode 15+.
bash
brew install xcodegen
git clone https://github.com/momenbasel/PureMac.git
cd PureMac
xcodegen generate
xcodebuild \
-project PureMac.xcodeproj \
-scheme PureMac \
-configuration Release \
-derivedDataPath build \
build
open build/Build/Products/Release/PureMac.app要求: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.mdPureMac/
├── PureMac/
│ ├── App/
│ │ └── PureMacApp.swift # App入口
│ ├── Views/
│ │ ├── ContentView.swift # 主窗口
│ │ ├── ScanView.swift # 智能扫描UI
│ │ ├── CategoryDetailView.swift # 分类详情页
│ │ └── SettingsView.swift # 计划任务与偏好设置
│ ├── Models/
│ │ ├── CleanCategory.swift # 分类定义
│ │ └── ScanResult.swift # 扫描结果模型
│ ├── Services/
│ │ ├── ScannerService.swift # 文件扫描逻辑
│ │ ├── CleanerService.swift # 删除逻辑
│ │ ├── SchedulerService.swift # 自动清理计划任务
│ │ └── PurgeableService.swift # APFS可释放空间处理
│ └── Utilities/
│ └── FileSizeFormatter.swift
├── project.yml # XcodeGen配置文件
└── CONTRIBUTING.mdCore Concepts
核心概念
Clean Categories
清理分类
PureMac operates on named categories, each mapping to specific filesystem paths:
| Category | Key Paths |
|---|---|
| System Junk | |
| User Cache | |
| Mail Attachments | |
| Trash | |
| Large & Old Files | |
| Purgeable Space | APFS Time Machine snapshots via |
| Xcode Junk | |
| Homebrew Cache | |
Large & Old Files are never auto-selected — the user must explicitly choose items before cleaning.
PureMac基于命名分类运行,每个分类对应特定的文件系统路径:
| 分类 | 关键路径 |
|---|---|
| System Junk(系统垃圾) | |
| User Cache(用户缓存) | |
| Mail Attachments(邮件附件) | |
| Trash(废纸篓) | |
| Large & Old Files(大文件与旧文件) | |
| Purgeable Space(可释放空间) | 通过 |
| Xcode Junk(Xcode垃圾) | |
| Homebrew Cache(Homebrew缓存) | |
大文件与旧文件永远不会被自动选中——用户必须明确选择项目后才能清理。
Working with the Codebase
代码库使用指南
Adding a New Clean Category
添加新的清理分类
- Define the category in :
CleanCategory.swift
swift
// CleanCategory.swift
enum 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"
}
}
}- Add scanning logic in :
ScannerService.swift
swift
// ScannerService.swift
func 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)
}- Add cleaning logic in :
CleanerService.swift
swift
// CleanerService.swift
func 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
}- 在中定义分类:
CleanCategory.swift
swift
// CleanCategory.swift
enum 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"
// 添加你的新分类:
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" // 新增
default: return "folder"
}
}
}- 在中添加扫描逻辑:
ScannerService.swift
swift
// ScannerService.swift
func scanCategory(_ category: CleanCategory) async throws -> ScanResult {
switch category {
case .gradleCache:
return try await scanPaths([
FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".gradle/caches")
])
// ...现有分支
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)
}- 在中添加清理逻辑:
CleanerService.swift
swift
// CleanerService.swift
func 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 {
// 记录日志但继续执行——不因单个文件删除失败而终止
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 implemented
import 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 = true
scheduler.autoPurgePurgeable = false
scheduler.schedulingEnabled = true
scheduler.scheduleIfNeeded()通过「设置」→「计划任务」标签页配置。时间间隔:每小时、3小时、6小时、12小时、每天、每周、每两周、每月。
swift
// SchedulerService.swift — 计划任务实现方式
import 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()
}
}
}以编程方式启用计划任务:
swift
let scheduler = SchedulerService()
scheduler.cleaningInterval = "weekly"
scheduler.autoCleanAfterScan = true
scheduler.autoPurgePurgeable = false
scheduler.schedulingEnabled = true
scheduler.scheduleIfNeeded()Purgeable Space (APFS Snapshots)
可释放空间(APFS快照)
PureMac uses to delete local Time Machine snapshots — this is the only operation requiring elevated privileges:
tmutilswift
// PurgeableService.swift
import 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) }
}
}
}PureMac使用删除本地Time Machine快照——这是唯一需要提升权限的操作:
tmutilswift
// PurgeableService.swift
import 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
Xcode缓存路径
swift
// Paths cleaned by the Xcode Junk category
let 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.
swift
// Xcode Junk分类清理的路径
let 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"),
]扫描这些路径并安全删除其内容,不会删除目录本身。
SwiftUI View Patterns
SwiftUI视图模式
Scan Progress View
扫描进度视图
swift
// Example: triggering a scan from a SwiftUI view
struct 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
}
}swift
// 示例:从SwiftUI视图触发扫描
struct 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 deselect
struct 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)
}
}
}swift
// 删除前显示文件——用户可以取消选择
struct 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)
配置(AppStorage键)
All preferences are stored in via :
UserDefaults@AppStorage| Key | Type | Default | Description |
|---|---|---|---|
| Bool | false | Enable scheduled cleaning |
| String | "daily" | Interval key (see above) |
| Bool | false | Auto-clean after scheduled scan |
| Bool | false | Auto-purge APFS snapshots |
Read/write from anywhere:
swift
UserDefaults.standard.set(true, forKey: "schedulingEnabled")
UserDefaults.standard.set("weekly", forKey: "cleaningInterval")所有偏好设置通过存储在中:
@AppStorageUserDefaults| 键 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| Bool | false | 启用定时清理 |
| String | "daily" | 时间间隔键(见上文) |
| Bool | false | 定时扫描后自动清理 |
| Bool | false | 自动释放APFS快照 |
可在任意位置读写:
swift
UserDefaults.standard.set(true, forKey: "schedulingEnabled")
UserDefaults.standard.set("weekly", forKey: "cleaningInterval")Building & Testing
构建与测试
bash
undefinedbash
undefinedGenerate Xcode project from project.yml
从project.yml生成Xcode项目
xcodegen generate
xcodegen generate
Build Release
构建Release版本
xcodebuild
-project PureMac.xcodeproj
-scheme PureMac
-configuration Release
-derivedDataPath build
build
-project PureMac.xcodeproj
-scheme PureMac
-configuration Release
-derivedDataPath build
build
xcodebuild
-project PureMac.xcodeproj
-scheme PureMac
-configuration Release
-derivedDataPath build
build
-project PureMac.xcodeproj
-scheme PureMac
-configuration Release
-derivedDataPath build
build
Run tests
运行测试
xcodebuild test
-project PureMac.xcodeproj
-scheme PureMac
-destination 'platform=macOS'
-project PureMac.xcodeproj
-scheme PureMac
-destination 'platform=macOS'
xcodebuild test
-project PureMac.xcodeproj
-scheme PureMac
-destination 'platform=macOS'
-project PureMac.xcodeproj
-scheme PureMac
-destination 'platform=macOS'
Open built app
打开已构建的应用
open build/Build/Products/Release/PureMac.app
---open build/Build/Products/Release/PureMac.app
---Contributing
贡献指南
- Fork and clone the repo.
- Run to create the
xcodegen generate..xcodeproj - Create a feature branch:
git checkout -b feature/gradle-cache-cleaning - Follow existing patterns in /
ScannerService.CleanerService - Never add network calls, analytics SDKs, or telemetry of any kind.
- Large & Old Files must never be auto-selected for deletion.
- Open a PR against .
main
See CONTRIBUTING.md for full guidelines.
- Fork并克隆仓库。
- 运行创建
xcodegen generate文件。.xcodeproj - 创建特性分支:
git checkout -b feature/gradle-cache-cleaning - 遵循/
ScannerService中的现有模式。CleanerService - 禁止添加网络请求、分析SDK或任何类型的遥测功能。
- 大文件与旧文件永远不能被自动选中进行删除。
- 针对分支提交PR。
main
完整指南请查看CONTRIBUTING.md。
Troubleshooting
故障排除
| Problem | Solution |
|---|---|
| |
| App blocked by Gatekeeper | The release build is notarized; if building from source, run |
| Purgeable scan returns 0 bytes | No local Time Machine snapshots exist — this is normal if TM is off |
| Xcode paths not found | Xcode has not been used yet or DerivedData was already cleared |
| Purgeable purge may prompt for admin credentials — this is expected macOS behavior |
| Scheduled cleaning not triggering | Ensure the app is running (it is not a background daemon); check Settings → Schedule |
| 问题 | 解决方案 |
|---|---|
| |
| 应用被Gatekeeper阻止 | 发布版本已通过公证;如果从源码构建,运行 |
| 可释放空间扫描返回0字节 | 不存在本地Time Machine快照——如果TM关闭则属于正常情况 |
| Xcode路径未找到 | Xcode尚未使用或DerivedData已被清除 |
| 释放可释放空间可能会提示管理员凭据——这是macOS的预期行为 |
| 定时清理未触发 | 确保应用正在运行(它不是后台守护进程);检查「设置」→「计划任务」 |
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 — no
FileManager.removeItem(at:)shell calls for regular cleaning.rm -rf
- 绝不删除系统关键文件或应用包。
- 仅删除缓存、日志、临时文件以及用户选择的项目。
- 大文件与旧文件需要用户明确选择才能删除。
- 可释放空间操作仅针对APFS Time Machine快照——不涉及可用空间。
- 所有文件系统操作使用——常规清理不使用
FileManager.removeItem(at:)shell命令。rm -rf