CleanCocoa/AsyncFileMonitor
Swift Concurrency wrapper for monitoring file system events using FSEvents
Features
- Modern Async/await: Uses
AsyncStreamfor natural async/await integration - Swift 6 Ready: Full concurrency support with
Sendableconformance - FSEvents Integration: Efficient file system monitoring using Apple's native FSEvents API
- Flexible Monitoring: Monitor single files, directories, or multiple paths
- Event Filtering: Rich event information with detailed change flags
Usage
Basic Usage
import AsyncFileMonitor
// Monitor a directory
let eventStream = FolderContentMonitor.makeStream(url: URL(fileURLWithPath: "/path/to/monitor/"))
// Use async/await to process events
for await event in eventStream {
print("File changed: \(event.filename) at \(event.eventPath)")
print("Change type: \(event.change)")
}Advanced Configuration
import AsyncFileMonitor
// Create a stream with custom configuration
let eventStream = FolderContentMonitor.makeStream(
url: URL(fileURLWithPath: "/Users/you/Documents"),
latency: 0.5 // Coalesce rapid changes
)
// Process file events
for await event in eventStream {
// Filter for file changes only
guard event.change.contains(.isFile) else { continue }
// Skip system files
guard event.filename != ".DS_Store" else { continue }
print("Document changed: \(event.filename)")
}Monitoring Multiple Paths
let eventStream = FolderContentMonitor.makeStream(paths: [
"/Users/you/Documents",
"/Users/you/Desktop"
])
for await event in eventStream {
print("Change in \(event.eventPath): \(event.change)")
}Task-based Processing
let eventStream = FolderContentMonitor.makeStream(url: folderURL)
let monitorTask = Task {
for await event in eventStream {
// Process file events
await handleFileChange(event)
}
}
// Stop monitoring
monitorTask.cancel()Filtering Events
let eventStream = FolderContentMonitor.makeStream(url: documentsURL)
for await event in eventStream
where event.change.contains(.isFile) && event.change.contains(.modified) {
await processModifiedFile(event.url)
}Multiple Concurrent Streams
AsyncFileMonitor uses a multicast AsyncStream approach where multiple streams from the same monitor share a single FSEventStream and receive identical events in registration order:
// Create multiple independent streams monitoring the same directory
let uiUpdateStream = FolderContentMonitor.makeStream(url: documentsURL)
let backupStream = FolderContentMonitor.makeStream(url: documentsURL)
let logStream = FolderContentMonitor.makeStream(url: documentsURL)
// Process events differently in each stream
Task {
for await event in uiUpdateStream {
await updateUI(for: event)
}
}
Task {
for await event in backupStream {
guard event.change.contains(.modified) else { continue }
await backupFile(event.url)
}
}
Task {
for await event in logStream {
logger.info("File changed: \(event.filename)")
}
}Ordering Guarantee: Events are delivered to subscribers in registration order. In the example above, for each file system event:
uiUpdateStreamreceives the event firstbackupStreamreceives the event secondlogStreamreceives the event third
You should probably not rely on the kind of things like subscription order, but I figured it's better you know just in case that you run into concurrency-related issues in your app, than having to guess.
Event Types
The Change struct provides detailed information about what changed:
File Type Flags
.isFile- The item is a regular file.isDirectory- The item is a directory.isSymlink- The item is a symbolic link.isHardlink- The item is a hard link
Change Type Flags
.created- Item was created.modified- Item was modified.removed- Item was removed.renamed- Item was renamed/moved
Metadata Changes
.changeOwner- Ownership changed.finderInfoModified- Finder info changed.inodeMetaModified- Inode metadata changed.xattrsModified- Extended attributes changed
Latency Configuration
Control event coalescing with the latency parameter:
// No latency - all events reported immediately (can be noisy)
let eventStream = FolderContentMonitor.makeStream(url: url, latency: 0.0)
// 1-second latency - coalesces rapid changes
let eventStream = FolderContentMonitor.makeStream(url: url, latency: 1.0)A latency of 0.0 can produce too much noise when applications make multiple rapid changes to files. Experiment with slightly higher values (e.g., 0.1-1.0 seconds) to reduce noise.
Understanding File Events
Different applications can generate different event patterns:
TextEdit (atomic saves):
texteditfile.txt changed (isFile, renamed, xattrsModified)
texteditfile.txt changed (isFile, renamed, finderInfoModified, xattrsModified)
texteditfile.txt.sb-56afa5c6-DmdqsL changed (isFile, renamed)
texteditfile.txt changed (isFile, renamed, finderInfoModified, inodeMetaModified, xattrsModified)
texteditfile.txt.sb-56afa5c6-DmdqsL changed (isFile, modified, removed, renamed, changeOwner)Simple editors (direct writes):
file.txt changed (isFile, modified, xattrsModified)Installation
Swift Package Manager
Add AsyncFileMonitor to your Package.swift:
dependencies: [
.package(url: "https://github.com/yourusername/AsyncFileMonitor.git", from: "1.0.0")
]Or add it through Xcode:
- File > Add Package Dependencies
- Enter the repository URL
- Select your target
Requirements
- macOS 14.0+
- Swift 6.0+
- Xcode 16.0+
Migration from RxFileMonitor
AsyncFileMonitor provides the same core functionality as RxFileMonitor but with modern Swift concurrency:
Before (RxFileMonitor)
import RxFileMonitor
import RxSwift
let monitor = FolderContentMonitor(url: folderUrl)
let disposeBag = DisposeBag()
monitor.rx.folderContentChange
.subscribe(onNext: { event in
print("File changed: \(event.filename)")
})
.disposed(by: disposeBag)After (AsyncFileMonitor)
import AsyncFileMonitor
let eventStream = FolderContentMonitor.makeStream(url: folderUrl)
for await event in eventStream {
print("File changed: \(event.filename)")
}Demo -- Command Line Tool
AsyncFileMonitor includes a built-in CLI tool for monitoring file changes to demo the capabilities:
# Monitor a single directory
swift run watch /Users/username/Documents
# Monitor multiple directories
swift run watch /path/to/folder1 /path/to/folder2
# Show usage help
swift run watchExample Output:
π― Starting AsyncFileMonitor CLI
π Monitoring paths:
β’ /Users/username/Documents
π‘ Press Ctrl+C to stop monitoring
[14:23:15.123] π /Users/username/Documents/test.txt
π isFile, modified
π Event ID: 12345678
[14:23:15.456] π /Users/username/Documents/newfile.txt
π isFile, created
π Event ID: 12345679Building and Testing
# Build
make build
# Build and run CLI tool
swift run watch /path/to/monitor
# Generate documentation
make docs
# Preview documentation in browser
make docs-preview
# Generate static documentation website
make docs-static
# Run tests
make test
# Format code
make format
# Clean
make cleanLicense
Copyright (c) 2016 Christian Tietze, RxSwiftCommunity (original RxFileMonitor) Copyright (c) 2025 Christian Tietze (AsyncFileMonitor modernization)
Distributed under The MIT License. See LICENSE file for details.
Package Metadata
Repository: CleanCocoa/AsyncFileMonitor
Stars: 47
Forks: 3
Open issues: 0
Default branch: main
Primary language: swift
License: MIT
README: README.md