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 watch
  • configuration: 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)
    }
}