import Foundation import CoreServices public protocol FolderWatcherDelegate: AnyObject { func folderWatcher(_ watcher: FolderWatcher, didDetectChanges paths: [String]) } public class FolderWatcher { public weak var delegate: FolderWatcherDelegate? private var stream: FSEventStreamRef? private let folderPath: String private var debounceTimer: Timer? private var pendingPaths: Set = [] private let queue = DispatchQueue(label: "com.cairnquire.folderwatcher") public init(folderPath: String) { self.folderPath = folderPath } public func start() { stop() let pathsToWatch = [folderPath as CFString] var context = FSEventStreamContext( version: 0, info: Unmanaged.passUnretained(self).toOpaque(), retain: nil, release: nil, copyDescription: nil ) let callback: FSEventStreamCallback = { (streamRef, clientCallBackInfo, numEvents, eventPaths, eventFlags, eventIds) in let watcher = Unmanaged.fromOpaque(clientCallBackInfo!).takeUnretainedValue() watcher.handleEvents(numEvents: numEvents, eventPaths: eventPaths, eventFlags: eventFlags) } stream = FSEventStreamCreate( kCFAllocatorDefault, callback, &context, pathsToWatch as CFArray, FSEventStreamEventId(kFSEventStreamEventIdSinceNow), 0.5, // latency in seconds FSEventStreamCreateFlags(kFSEventStreamCreateFlagFileEvents | kFSEventStreamCreateFlagUseCFTypes) ) if let stream = stream { FSEventStreamSetDispatchQueue(stream, queue) FSEventStreamStart(stream) } } public func stop() { if let stream = stream { FSEventStreamStop(stream) FSEventStreamInvalidate(stream) FSEventStreamRelease(stream) self.stream = nil } } private func handleEvents(numEvents: Int, eventPaths: UnsafeMutableRawPointer, eventFlags: UnsafePointer) { guard let paths = Unmanaged.fromOpaque(eventPaths).takeUnretainedValue() as? [String] else { return } var changedPaths: [String] = [] for i in 0..