Advanced Usage Guide
May 24, 2026 · View on GitHub
Advanced Filtering Techniques
Complex Filter Combinations
You can combine multiple filters using boolean logic:
let complexFilter = FileFilter.imageFiles
.and(.fileSize(1024...))
.and(.modifiedWithin(3600))
.or(.fileExtensions(["pdf"]))
watcher.addFilter(complexFilter)
Custom Predicate Filters
Create sophisticated custom filters:
let advancedFilter = FileFilter.custom { url in
// Only process files that are not currently being written to
guard let resourceValues = try? url.resourceValues(forKeys: [.contentModificationDateKey]) else {
return false
}
// Allow files that haven't been modified in the last 2 seconds
let timeSinceModification = Date().timeIntervalSince(resourceValues.contentModificationDate ?? Date.distantPast)
return timeSinceModification > 2.0
}
Filter Chain Strategies
Different filter chain approaches:
// AND strategy (all filters must match)
filterChain.add(.imageFiles)
filterChain.add(.fileSize(1024...))
let andResults = filterChain.filter(urls)
// OR strategy (any filter can match)
let orResults = filterChain.filterAny(urls)
Predictive Ignoring Patterns
Image Processing Pipeline
Prevent monitoring your own output files:
let imageProcessor = FileTransformPredictor.imageCompression(suffix: "_optimized")
config.transformPredictor = imageProcessor
watcher.onFilteredChange = { newImages in
for image in newImages {
// Predict and ignore output files before processing
let predictedOutputs = imageProcessor.predictOutputFiles(for: image)
watcher.addPredictiveIgnore(predictedOutputs)
// Process image
processImage(image) { outputURL in
// Add actual output to ignore list
watcher.addIgnoredFiles([outputURL])
}
}
}
Build System Integration
Ignore generated build artifacts:
let buildPredictor = FileTransformPredictor(rules: [
.init(inputPattern: ".*\\.swift$", outputTemplate: "build/{name}.o"),
.init(inputPattern: ".*\\.m$", outputTemplate: "build/{name}.o"),
.init(inputPattern: ".*\\.c$", outputTemplate: "build/{name}.o")
])
config.transformPredictor = buildPredictor
Recursive Monitoring Strategies
Project Structure Monitoring
Monitor a development project with intelligent exclusions:
var options = RecursiveWatchOptions()
options.maxDepth = 10
options.followSymlinks = false
options.maxWatchedDirectories = 256 // FD ceiling for sandboxed processes
options.backend = .dispatchSource
options.excludePatterns = [
".git",
"node_modules",
"build",
"*.xcworkspace",
"DerivedData",
".build",
"Pods"
]
let projectWatcher = try RecursiveDirectoryWatcher(
url: projectURL,
options: options
)
// Only watch source files
projectWatcher.addFilter(
.fileExtensions(["swift", "m", "h", "cpp", "c"])
.and(.fileSize(1...)) // Ignore empty files
)
Large macOS Asset Trees
Use the FSEvents backend for large photo, asset, or cloud-synced directory trees on macOS:
let options = RecursiveWatchOptions(
maxDepth: 3,
excludePatterns: [".git", ".build", "node_modules"],
backend: .fsevents
)
let assetWatcher = try RecursiveDirectoryWatcher(url: assetRoot, options: options)
assetWatcher.addFilter(.fileExtensions(["jpg", "jpeg", "png", "webp", "heic"]))
assetWatcher.start(on: .global(qos: .utility))
This backend uses one root FSEvent stream and does not consume one file
descriptor per subdirectory. watchedDirectories returns the watched root URL.
For event-driven automation on very large trees, consume exact file-level events and disable directory snapshots:
var configuration = DirectoryWatcher.Configuration()
configuration.scansChangedDirectoriesForFilteredEvents = false
let assetWatcher = try RecursiveDirectoryWatcher(
url: assetRoot,
options: options,
configuration: configuration
)
assetWatcher.onFileChange = { event in
guard event.itemKind == .file, event.eventType != .deleted else { return }
processChangedAsset(event.url)
}
assetWatcher.start()
Content Management System
Monitor content directories with smart categorization:
let contentWatcher = try RecursiveDirectoryWatcher(url: contentRoot)
// Different handlers for different content types
contentWatcher.onFilteredChange = { changedFiles in
let images = changedFiles.filter { FileFilter.imageFiles.matches(\$0) }
let documents = changedFiles.filter { FileFilter.documentFiles.matches(\$0) }
let videos = changedFiles.filter { FileFilter.videoFiles.matches(\$0) }
if !images.isEmpty {
processImages(images)
}
if !documents.isEmpty {
processDocuments(documents)
}
if !videos.isEmpty {
processVideos(videos)
}
}
Performance Optimization
Debouncing Strategies
Adjust debouncing for different scenarios:
// High-frequency scenarios (log processing)
var highFreqConfig = DirectoryWatcher.Configuration()
highFreqConfig.debounceInterval = 0.1
// Batch processing scenarios
var batchConfig = DirectoryWatcher.Configuration()
batchConfig.debounceInterval = 2.0
// Real-time scenarios
var realtimeConfig = DirectoryWatcher.Configuration()
realtimeConfig.debounceInterval = 0.01
Queue Management
Use appropriate queues for different workloads:
// CPU-intensive processing
config.queue = .global(qos: .userInitiated)
// Background processing
config.queue = .global(qos: .background)
// UI-related updates
config.queue = .main
Memory Management
Implement cleanup strategies for long-running watchers:
class LongRunningWatcher {
private let watcher: DirectoryWatcher
private var cleanupTimer: Timer?
init(url: URL) throws {
self.watcher = try DirectoryWatcher(url: url)
// Cleanup ignore list every hour
cleanupTimer = Timer.scheduledTimer(withTimeInterval: 3600, repeats: true) { _ in
self.watcher.configuration.ignoreList.cleanup()
}
}
deinit {
cleanupTimer?.invalidate()
watcher.stop()
}
}
Error Handling and Recovery
Robust Error Handling
Implement comprehensive error handling:
class RobustFileWatcher {
private var watcher: DirectoryWatcher?
private let watchURL: URL
private let maxRetries = 3
private var retryCount = 0
init(url: URL) {
self.watchURL = url
startWatching()
}
private func startWatching() {
do {
watcher = try DirectoryWatcher(url: watchURL)
watcher?.onError = { [weak self] error in
self?.handleError(error)
}
watcher?.onDirectoryChange = { [weak self] url in
self?.resetRetryCount()
self?.processChange(at: url)
}
watcher?.start()
} catch {
handleError(error)
}
}
private func handleError(_ error: Error) {
print("Watcher error: \(error)")
guard retryCount < maxRetries else {
print("Max retries exceeded, giving up")
return
}
retryCount += 1
// Exponential backoff
let delay = pow(2.0, Double(retryCount))
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
self.startWatching()
}
}
private func resetRetryCount() {
retryCount = 0
}
}
Permission Handling
Handle permission-related errors gracefully:
watcher.onError = { error in
switch error {
case .insufficientPermissions(let url):
// Request permission or show user guidance
requestFileSystemPermission(for: url)
case .directoryNotFound(let url):
// Wait for directory to be created
waitForDirectory(url)
case .cannotOpenDirectory(let url):
// Check if directory is accessible
verifyDirectoryAccess(url)
case .tooManyWatchers(let limit):
// Hit FD ceiling — deeper subdirectories are not being watched
print("Watcher ceiling reached (\(limit)); consider reducing maxDepth or maxWatchedDirectories")
case .failedToWatch(let url, let underlying):
// Single directory failed to open during recursive scan
print("Failed to watch \(url.path): \(underlying.localizedDescription)")
default:
// Handle other errors
print("Unhandled error: \(error)")
}
}
Integration with Other Systems
Combine Integration Patterns
Advanced Combine usage:
import Combine
class CombineWatcherService {
private let watcher: DirectoryWatcher
private var cancellables = Set<AnyCancellable>()
// Processed file publisher
lazy var processedFiles = watcher.filteredChangePublisher
.flatMap { files in
Publishers.Sequence(sequence: files)
}
.filter { file in
// Additional filtering
file.pathExtension == "jpg"
}
.map { file in
// Transform to processed format
ProcessedFile(url: file)
}
.eraseToAnyPublisher()
// Batch processing
lazy var batchedChanges = watcher.directoryChangePublisher
.collect(.byTime(DispatchQueue.main, .seconds(5)))
.filter { !\$0.isEmpty }
.eraseToAnyPublisher()
init(url: URL) throws {
self.watcher = try DirectoryWatcher(url: url)
setupSubscriptions()
}
private func setupSubscriptions() {
processedFiles
.sink { processedFile in
self.handleProcessedFile(processedFile)
}
.store(in: &cancellables)
batchedChanges
.sink { batch in
self.processBatch(batch)
}
.store(in: &cancellables)
}
}
Swift Concurrency Patterns
Modern async/await integration:
actor FileProcessor {
private let watcher: RecursiveDirectoryWatcher
private var isProcessing = false
init(url: URL, options: RecursiveWatchOptions = RecursiveWatchOptions()) throws {
self.watcher = try RecursiveDirectoryWatcher(url: url, options: options)
startProcessing()
}
private func startProcessing() {
Task {
// Async start — scan runs on a background queue, returns when complete
await watcher.startAsync()
for await url in watcher.directoryChanges {
await processDirectory(url)
}
}
Task {
for await files in watcher.filteredChanges {
await processFiles(files)
}
}
}
Task {
for await files in watcher.filteredChanges {
await processFiles(files)
}
}
}
private func processDirectory(_ url: URL) async {
// Process directory change
print("Processing directory: \(url)")
}
private func processFiles(_ files: [URL]) async {
guard !isProcessing else { return }
isProcessing = true
defer { isProcessing = false }
// Process files concurrently
await withTaskGroup(of: Void.self) { group in
for file in files {
group.addTask {
await self.processFile(file)
}
}
}
}
private func processFile(_ file: URL) async {
// Async file processing
print("Processing file: \(file)")
}
}
Testing and Debugging
Unit Testing Watchers
Create testable watcher components:
class TestableWatcher {
let watcher: DirectoryWatcher
var events: [FileSystemEvent] = []
init(url: URL) throws {
self.watcher = try DirectoryWatcher(url: url)
watcher.delegate = self
}
}
extension TestableWatcher: DirectoryWatcherDelegate {
func directoryDidChange(with event: FileSystemEvent) {
events.append(event)
}
func directoryDidChange(at url: URL) {
events.append(FileSystemEvent(url: url, eventType: .modified))
}
}
// Usage in tests
func testWatcherDetectsChanges() {
let tempDir = createTempDirectory()
let testWatcher = try! TestableWatcher(url: tempDir)
testWatcher.watcher.start()
// Create test file
let testFile = tempDir.appendingPathComponent("test.txt")
try! "content".write(to: testFile, atomically: true, encoding: .utf8)
// Wait for event
wait(timeout: 2.0) {
testWatcher.events.count > 0
}
XCTAssertEqual(testWatcher.events.count, 1)
XCTAssertEqual(testWatcher.events.first?.url, tempDir)
}
Performance Monitoring
Monitor watcher performance:
class MonitoredWatcher {
private let watcher: DirectoryWatcher
private var eventCount = 0
private var startTime = Date()
init(url: URL) throws {
self.watcher = try DirectoryWatcher(url: url)
watcher.onDirectoryChange = { [weak self] url in
self?.recordEvent()
self?.processChange(url)
}
}
private func recordEvent() {
eventCount += 1
let elapsed = Date().timeIntervalSince(startTime)
let eventsPerSecond = Double(eventCount) / elapsed
print("Events/sec: \(eventsPerSecond)")
// Log if performance degrades
if eventsPerSecond > 100 {
print("⚠️ High event frequency detected")
}
}
}