Using my apps is also a way to support me:
A high-performance, Swift-native file system watcher for macOS and iOS that provides intelligent monitoring of directory changes with minimal system resource usage.
✨ Event-Driven Architecture - Uses DispatchSource
for efficient file system monitoring
🎯 Smart Filtering - Advanced filter chains with support for file types, sizes, and patterns
🔍 Predictive Ignoring - Avoid monitoring self-generated files
📁 Recursive Monitoring - Watch entire directory trees with configurable depth
⚡ Modern Swift - Full support for Combine, Swift Concurrency, and structured concurrency
🛡️ Thread-Safe - Designed for concurrent use across multiple threads
📊 Low Resource Usage - Minimal CPU and memory footprint
Add FSWatcher to your project through Xcode or by adding it to your Package.swift
:
dependencies: [
.package(url: "https://github.com/okooo5km/FSWatcher.git", from: "1.0.0")
]
import FSWatcher
// Create a watcher for a directory
let watcher = try DirectoryWatcher(url: URL(fileURLWithPath: "/Users/user/Documents"))
// Set up event handler
watcher.onDirectoryChange = { url in
print("Directory changed: \\(url.path)")
}
// Start watching
watcher.start()
// Watch only image files larger than 1KB
var config = DirectoryWatcher.Configuration()
config.filterChain.add(.imageFiles)
config.filterChain.add(.fileSize(1024...))
let watcher = try DirectoryWatcher(url: watchURL, configuration: config)
watcher.onFilteredChange = { imageFiles in
print("New images: \\(imageFiles.map { $0.lastPathComponent })")
}
let multiWatcher = MultiDirectoryWatcher()
multiWatcher.onDirectoryChange = { url in
print("Change in: \\(url.path)")
}
multiWatcher.startWatching(directories: [documentsURL, downloadsURL])
var options = RecursiveWatchOptions()
options.maxDepth = 5
options.excludePatterns = ["node_modules", ".git", "*.tmp"]
let recursiveWatcher = try RecursiveDirectoryWatcher(
url: projectURL,
options: options
)
FSWatcher provides a powerful filtering system that can be chained together:
// Combine multiple filters
watcher.addFilter(
.fileExtensions(["swift", "m"])
.and(.fileSize(1000...))
.and(.modifiedWithin(3600))
)
// Pre-built filter types
config.filterChain.add(.imageFiles) // Images
config.filterChain.add(.videoFiles) // Videos
config.filterChain.add(.documentFiles) // Documents
config.filterChain.add(.directoriesOnly) // Directories only
Prevent monitoring your own output files:
// Set up a transform predictor
let predictor = FileTransformPredictor.imageCompression(suffix: "_compressed")
config.transformPredictor = predictor
// The watcher will automatically ignore predicted output files
watcher.onFilteredChange = { newImages in
for image in newImages {
compressImage(image) // Output will be automatically ignored
}
}
import Combine
watcher.directoryChangePublisher
.debounce(for: .seconds(1), scheduler: RunLoop.main)
.sink { url in
print("Debounced change: \\(url.path)")
}
.store(in: &cancellables)
watcher.start()
for await url in watcher.directoryChanges {
await processChange(at: url)
}
var config = DirectoryWatcher.Configuration()
// Debounce interval (default: 0.5 seconds)
config.debounceInterval = 1.0
// File system events to monitor
config.eventMask = [.write, .extend, .delete, .rename]
// Processing queue
config.queue = .global(qos: .userInitiated)
// Filter chain
config.filterChain.add(.imageFiles)
// Ignore list management
config.ignoreList.addIgnorePattern("*.tmp")
// Transform prediction
config.transformPredictor = FileTransformPredictor.imageCompression()
watcher.onError = { error in
switch error {
case .cannotOpenDirectory(let url):
print("Cannot open: \\(url.path)")
case .insufficientPermissions(let url):
print("Permission denied: \\(url.path)")
case .directoryNotFound(let url):
print("Not found: \\(url.path)")
default:
print("Error: \\(error)")
}
}
Perfect for building image compression tools like Zipic:
let pipeline = try ImageCompressionPipeline(
watchDirectory: URL(fileURLWithPath: "/Users/user/ToCompress"),
compressionQuality: 0.8
)
pipeline.start()
Monitor source code changes:
let projectWatcher = try RecursiveDirectoryWatcher(url: projectURL)
projectWatcher.addFilter(.fileExtensions(["swift", "js", "css"]))
projectWatcher.onFilteredChange = { changedFiles in
triggerHotReload(for: changedFiles)
}
let backupWatcher = try DirectoryWatcher(url: documentsURL)
backupWatcher.addFilter(.modifiedWithin(300)) // Last 5 minutes
backupWatcher.onFilteredChange = { recentFiles in
performIncrementalBackup(files: recentFiles)
}
- Event-driven: Only processes actual file system events, no polling
- Debounced: Prevents excessive event handling during rapid changes
- Filtered: Process only relevant files using efficient filter chains
- Resource management: Automatic cleanup of file descriptors and resources
FSWatcher is designed to be thread-safe:
- All public APIs can be called from any thread
- Internal state is protected with appropriate synchronization
- Event handlers are called on the configured dispatch queue
- macOS: 12.0+
- iOS: 15.0+
- Swift: 5.9+
- Xcode: 14.0+
The Examples/
directory contains complete, runnable examples:
BasicUsage.swift
- Fundamental usage patternsImageCompression.swift
- Complete image processing pipelineHotReload.swift
- Development tool integration
Contributions are welcome! Please read our Contributing Guide for details.
FSWatcher is available under the MIT license. See the LICENSE file for more info.
FSWatcher was inspired by the successful file monitoring implementation in Zipic, a popular image compression tool for macOS. The design focuses on performance, reliability, and developer experience learned from real-world usage.
Made with ❤️ for the Swift community