FSWatcher API Documentation
May 24, 2026 · View on GitHub
Core Classes
DirectoryWatcher
The main class for monitoring changes in a single directory.
Initialization
public init(url: URL, configuration: Configuration = Configuration()) throws
Parameters:
url: The directory URL to watchconfiguration: Configuration options for the watcher
Throws: FSWatcherError if initialization fails
Properties
// Event handlers
public weak var delegate: DirectoryWatcherDelegate?
public var onDirectoryChange: ((URL) -> Void)?
public var onFilteredChange: (([URL]) -> Void)?
public var onError: ((FSWatcherError) -> Void)?
// Combine support
public var directoryChangePublisher: AnyPublisher<URL, Never> { get }
public var filteredChangePublisher: AnyPublisher<[URL], Never> { get }
// Swift Concurrency support
public var directoryChanges: AsyncStream<URL> { get }
public var filteredChanges: AsyncStream<[URL]> { get }
// State
public var isWatching: Bool { get }
Methods
public func start()
public func stop()
public func addFilter(_ filter: FileFilter)
public func clearFilters()
public func addIgnoredFiles(_ urls: [URL])
public func addPredictiveIgnore(_ urls: [URL])
MultiDirectoryWatcher
Manages monitoring of multiple directories simultaneously.
MultiDirectoryWatcher - Initialization
public init(configuration: DirectoryWatcher.Configuration = DirectoryWatcher.Configuration())
MultiDirectoryWatcher - Methods
public func startWatching(directories: [URL])
public func startWatching(directory: URL)
public func stopWatching(directory: URL)
public func stopAllWatching()
public func isWatching(directory: URL) -> Bool
// Properties
public var watchedDirectories: [URL] { get }
public var isWatching: Bool { get }
RecursiveDirectoryWatcher
Monitors directories and their subdirectories recursively.
RecursiveDirectoryWatcher - Initialization
public init(url: URL, options: RecursiveWatchOptions = RecursiveWatchOptions(), configuration: DirectoryWatcher.Configuration = DirectoryWatcher.Configuration()) throws
RecursiveDirectoryWatcher - Methods
/// Synchronous start — blocks the calling thread during the recursive scan.
public func start()
/// Async start — scan runs on the given queue; safe to call from the main thread.
public func start(on queue: DispatchQueue = .global(qos: .utility))
/// async/await variant — returns once the initial scan has completed.
public func startAsync(on queue: DispatchQueue = .global(qos: .utility)) async
/// Stop watching all directories.
public func stop()
RecursiveDirectoryWatcher - Events
public weak var delegate: DirectoryWatcherDelegate?
public var onDirectoryChange: ((URL) -> Void)?
public var onFilteredChange: (([URL]) -> Void)?
public var onFileChange: ((FileSystemEvent) -> Void)?
public var onError: ((FSWatcherError) -> Void)?
public var directoryChangePublisher: AnyPublisher<URL, Never> { get }
public var filteredChangePublisher: AnyPublisher<[URL], Never> { get }
public var fileChangePublisher: AnyPublisher<FileSystemEvent, Never> { get }
public var directoryChanges: AsyncStream<URL> { get }
public var filteredChanges: AsyncStream<[URL]> { get }
public var fileChanges: AsyncStream<FileSystemEvent> { get }
RecursiveWatchOptions
public enum RecursiveWatchBackend {
case automatic
case dispatchSource
case fsevents
}
public struct RecursiveWatchOptions {
public var maxDepth: Int? = nil
public var followSymlinks: Bool = false
public var excludePatterns: [String] = []
public var maxWatchedDirectories: Int = 256 // FD ceiling
public var backend: RecursiveWatchBackend = .dispatchSource
public init()
public init(maxDepth: Int? = nil, followSymlinks: Bool = false,
excludePatterns: [String] = [], maxWatchedDirectories: Int = 256)
public init(maxDepth: Int? = nil, followSymlinks: Bool = false,
excludePatterns: [String] = [], maxWatchedDirectories: Int = 256,
backend: RecursiveWatchBackend)
}
backend defaults to .dispatchSource to preserve FSWatcher 0.1.x behavior.
Use .fsevents for large recursive macOS trees where a single FSEvent stream is
preferable to one file descriptor per subdirectory. Use .automatic to select
FSEvents on macOS unless followSymlinks is enabled; other platforms use
DispatchSource.
When FSEvents is active, watchedDirectories returns the watched root URL.
maxWatchedDirectories applies only to the DispatchSource backend.
File-level events are available through onFileChange,
fileChangePublisher, and fileChanges. They report the exact URL, item kind,
event type, raw flags, and event ID when the OS provides them.
MultiRecursiveDirectoryWatcher
Manages recursive monitoring of multiple directories simultaneously.
MultiRecursiveDirectoryWatcher - Initialization
public init(options: RecursiveWatchOptions = RecursiveWatchOptions(), configuration: DirectoryWatcher.Configuration = DirectoryWatcher.Configuration())
MultiRecursiveDirectoryWatcher - Methods
public func startWatching(directories: [URL])
public func startWatching(directory: URL)
public func stopWatching(directory: URL)
public func stopAllWatching()
public func isWatching(directory: URL) -> Bool
// Filter management
public func addFilter(_ filter: FileFilter)
public func addFilter(_ filter: FileFilter, to directory: URL)
public func clearAllFilters()
// Ignore list management
public func addIgnoredFiles(_ urls: [URL])
public func addIgnoredFiles(_ urls: [URL], in directory: URL)
public func addPredictiveIgnore(_ urls: [URL])
public func addPredictiveIgnore(_ urls: [URL], in directory: URL)
// Properties
public var watchedDirectories: [URL] { get }
public var allWatchedDirectories: [URL] { get }
public var isWatching: Bool { get }
public var onFileChange: ((FileSystemEvent) -> Void)?
public var fileChangePublisher: AnyPublisher<FileSystemEvent, Never> { get }
public var fileChanges: AsyncStream<FileSystemEvent> { get }
Configuration
DirectoryWatcher.Configuration
public struct Configuration {
public var debounceInterval: TimeInterval = 0.5
public var eventMask: DispatchSource.FileSystemEvent = [.write, .extend, .delete, .rename]
public var queue: DispatchQueue = .global(qos: .utility)
public var filterChain: FilterChain = FilterChain()
public var ignoreList: IgnoreList = IgnoreList()
public var transformPredictor: FileTransformPredictor?
public var scansChangedDirectoriesForFilteredEvents: Bool = true
public init()
}
Set scansChangedDirectoriesForFilteredEvents = false when you consume exact
file-level events and want to avoid directory listing work. Exact FSEvents file
events are still emitted through onFileChange.
FileSystemEvent
public struct FileSystemEvent {
public enum ItemKind: Equatable, Sendable {
case file
case directory
case symbolicLink
case unknown
}
public enum EventType: Equatable, Sendable {
case created
case modified
case deleted
case renamed
case unknown
}
public let url: URL
public let eventType: EventType
public let timestamp: Date
public let itemKind: ItemKind
public let requiresRescan: Bool
public let rawFlags: UInt32
public let eventID: UInt64?
}
Filtering System
FileFilter
public struct FileFilter {
// Predefined filters
public static func fileExtensions(_ extensions: [String]) -> FileFilter
public static func utTypes(_ types: [UTType]) -> FileFilter
public static func fileName(matching pattern: String) -> FileFilter
public static func fileSize(_ range: ClosedRange<Int>) -> FileFilter
public static func modifiedWithin(_ interval: TimeInterval) -> FileFilter
public static func custom(_ predicate: @escaping (URL) -> Bool) -> FileFilter
// Convenience filters
public static var imageFiles: FileFilter
public static var videoFiles: FileFilter
public static var audioFiles: FileFilter
public static var documentFiles: FileFilter
public static var directoriesOnly: FileFilter
public static var filesOnly: FileFilter
// Combinators
public func and(_ other: FileFilter) -> FileFilter
public func or(_ other: FileFilter) -> FileFilter
public func not() -> FileFilter
}
FilterChain
public struct FilterChain {
public init()
public init(filters: [FileFilter])
public mutating func add(_ filter: FileFilter)
public mutating func clear()
public var isEmpty: Bool { get }
public var count: Int { get }
public func matches(_ url: URL) -> Bool
public func matchesAny(_ url: URL) -> Bool
public func filter(_ urls: [URL]) -> [URL]
public func filterAny(_ urls: [URL]) -> [URL]
}
Ignore System
IgnoreList
public class IgnoreList {
public init()
public init(ignoredFiles: [URL])
// Managing ignored files
public func addIgnored(_ urls: [URL])
public func addIgnored(_ url: URL)
public func removeIgnored(_ urls: [URL])
public func removeIgnored(_ url: URL)
// Predictive ignoring
public func addPredictiveIgnore(_ urls: [URL])
public func addPredictiveIgnore(_ url: URL)
public func removePredictiveIgnore(_ urls: [URL])
// Pattern-based ignoring
public func addIgnorePattern(_ pattern: String)
public func addIgnorePatterns(_ patterns: [String])
public func removeIgnorePattern(_ pattern: String)
// Checking ignore status
public func shouldIgnore(_ url: URL) -> Bool
// Maintenance
public func cleanup()
public func clear()
public func clearIgnored()
public func clearPredictive()
public func clearPatterns()
// Properties
public var ignoredCount: Int { get }
public var predictiveCount: Int { get }
public var patternCount: Int { get }
}
FileTransformPredictor
public struct FileTransformPredictor {
public struct TransformRule {
public let inputPattern: String
public let outputTemplate: String
public let formatChange: Bool
public init(inputPattern: String, outputTemplate: String, formatChange: Bool = false)
}
public init(rules: [TransformRule])
public init(rule: TransformRule)
public func predictOutputFiles(for inputURL: URL) -> [URL]
public func predictOutputFiles(for inputURLs: [URL]) -> [URL]
// Factory methods
public static func imageCompression(suffix: String = "_compressed") -> FileTransformPredictor
public static func formatConversion(from: String, to: String) -> FileTransformPredictor
public static func thumbnailGeneration(prefix: String = "thumb_", size: String = "") -> FileTransformPredictor
public static func videoTranscoding(outputFormat: String = "mp4") -> FileTransformPredictor
public static func documentConversion() -> FileTransformPredictor
}
Events and Protocols
FileSystemEvent
public struct FileSystemEvent {
public let url: URL
public let eventType: EventType
public let timestamp: Date
public enum EventType {
case created
case modified
case deleted
case renamed
case unknown
}
public init(url: URL, eventType: EventType, timestamp: Date = Date())
}
DirectoryWatcherDelegate
public protocol DirectoryWatcherDelegate: AnyObject {
func directoryDidChange(at url: URL)
func directoryDidChange(with event: FileSystemEvent)
}
Error Handling
FSWatcherError
public enum FSWatcherError: Error, LocalizedError {
case cannotOpenDirectory(URL)
case insufficientPermissions(URL)
case directoryNotFound(URL)
case systemResourcesUnavailable
case invalidConfiguration(String)
case tooManyWatchers(limit: Int) // FD ceiling reached
case failedToWatch(URL, underlying: Error) // single directory open failure
public var errorDescription: String? { get }
}
Extensions
URL+Extensions
extension URL {
public var isDirectory: Bool { get }
public var isRegularFile: Bool { get }
public var fileExists: Bool { get }
public var fileSize: Int? { get }
public var modificationDate: Date? { get }
public var creationDate: Date? { get }
public var isSymbolicLink: Bool { get }
public var isHidden: Bool { get }
public func subdirectories(includingHidden: Bool = false) -> [URL]
public func files(includingHidden: Bool = false) -> [URL]
public func contents(includingHidden: Bool = false) -> [URL]
}
Usage Patterns
Basic Usage
let watcher = try DirectoryWatcher(url: directoryURL)
watcher.onDirectoryChange = { url in
print("Directory changed: \(url)")
}
watcher.start()
With Filters
var config = DirectoryWatcher.Configuration()
config.filterChain.add(.imageFiles)
config.filterChain.add(.fileSize(1024...))
let watcher = try DirectoryWatcher(url: directoryURL, configuration: config)
watcher.onFilteredChange = { files in
print("Filtered files: \(files)")
}
With Combine
watcher.directoryChangePublisher
.debounce(for: .seconds(1), scheduler: RunLoop.main)
.sink { url in
print("Debounced change: \(url)")
}
.store(in: &cancellables)
With Swift Concurrency
Task {
for await url in watcher.directoryChanges {
await processChange(at: url)
}
}