How would I monitor a folder for new files in swift, without polling (which is very inefficient)? I\'ve heard of APIs such as kqueue and FSEvents - but I\'m not sure it\'s p
Easiest method I've found that I'm currently using is this wonderful library: https://github.com/eonist/FileWatcher
From README
pod "FileWatcher"
github "eonist/FileWatcher" "master"
FileWatcherExample.xcodeproj
let filewatcher = FileWatcher([NSString(string: "~/Desktop").expandingTildeInPath])
filewatcher.callback = { event in
print("Something happened here: " + event.path)
}
filewatcher.start() // start monitoring
SKQueue is a Swift wrapper around kqueue. Here is sample code that watches a directory and notifies of write events.
class SomeClass: SKQueueDelegate {
func receivedNotification(_ notification: SKQueueNotification, path: String, queue: SKQueue) {
print("\(notification.toStrings().map { $0.rawValue }) @ \(path)")
}
}
if let queue = SKQueue() {
let delegate = SomeClass()
queue.delegate = delegate
queue.addPath("/some/file/or/directory")
queue.addPath("/some/other/file/or/directory")
}
GCD seems to be the way to go. NSFilePresenter
classes doesn't work properly. They're buggy, broken, and Apple is haven't willing to fix them for last 4 years. Likely to be deprecated.
Here's a very nice posting which describes essentials of this technique.
"Handling Filesystem Events with GCD", by David Hamrick.
Sample code cited from the website. I translated his C code into Swift.
let fildes = open("/path/to/config.plist", O_RDONLY)
let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
let source = dispatch_source_create(
DISPATCH_SOURCE_TYPE_VNODE,
UInt(fildes),
DISPATCH_VNODE_DELETE | DISPATCH_VNODE_WRITE | DISPATCH_VNODE_EXTEND | DISPATCH_VNODE_ATTRIB | DISPATCH_VNODE_LINK | DISPATCH_VNODE_RENAME | DISPATCH_VNODE_REVOKE,
queue)
dispatch_source_set_event_handler(source,
{
//Reload the config file
})
dispatch_source_set_cancel_handler(source,
{
//Handle the cancel
})
dispatch_resume(source);
...
// sometime later
dispatch_source_cancel(source);
For reference, here're another QAs posted by the author:
If you're interested in watching directories, here's another posting which describes it.
"Monitoring a Folder with GCD" on Cocoanetics. (unfortunately, I couldn't find the author's name. I am sorry for lacking attribution)
The only noticeable difference is getting a file-descriptor. This makes event-notification-only file descriptor for a directory.
_fileDescriptor = open(path.fileSystemRepresentation(), O_EVTONLY)
Previously I claimed FSEvents
API is not working, but I was wrong. The API is working very well, and if you're interested in watching on deep file tree, than it can be better then GCD by its simplicity.
Anyway, FSEvents cannot be used in pure Swift programs. Because it requires passing of C callback function, and Swift does not support it currently (Xcode 6.1.1). Then I had to fallback to Objective-C and wrap it again.
Also, any of this kind API is all fully asynchronous. That means actual file system state can be different at the time you are receiving the notifications. Then precise or accurate notification is not really helpful, and useful only for marking a dirty flag.
I finally ended up with writing a wrapper around FSEvents
for Swift.
Here's my work, and I hope this to be helpful.
Swift 5 Version for Directory Monitor, with GCD, original from Apple
import Foundation
/// A protocol that allows delegates of `DirectoryMonitor` to respond to changes in a directory.
protocol DirectoryMonitorDelegate: class {
func directoryMonitorDidObserveChange(directoryMonitor: DirectoryMonitor)
}
class DirectoryMonitor {
// MARK: Properties
/// The `DirectoryMonitor`'s delegate who is responsible for responding to `DirectoryMonitor` updates.
weak var delegate: DirectoryMonitorDelegate?
/// A file descriptor for the monitored directory.
var monitoredDirectoryFileDescriptor: CInt = -1
/// A dispatch queue used for sending file changes in the directory.
let directoryMonitorQueue = DispatchQueue(label: "directorymonitor", attributes: .concurrent)
/// A dispatch source to monitor a file descriptor created from the directory.
var directoryMonitorSource: DispatchSource?
/// URL for the directory being monitored.
var url: URL
// MARK: Initializers
init(url: URL) {
self.url = url
}
// MARK: Monitoring
func startMonitoring() {
// Listen for changes to the directory (if we are not already).
if directoryMonitorSource == nil && monitoredDirectoryFileDescriptor == -1 {
// Open the directory referenced by URL for monitoring only.
monitoredDirectoryFileDescriptor = open((url as NSURL).fileSystemRepresentation, O_EVTONLY)
// Define a dispatch source monitoring the directory for additions, deletions, and renamings.
directoryMonitorSource = DispatchSource.makeFileSystemObjectSource(fileDescriptor: monitoredDirectoryFileDescriptor, eventMask: DispatchSource.FileSystemEvent.write, queue: directoryMonitorQueue) as? DispatchSource
// Define the block to call when a file change is detected.
directoryMonitorSource?.setEventHandler{
// Call out to the `DirectoryMonitorDelegate` so that it can react appropriately to the change.
self.delegate?.directoryMonitorDidObserveChange(directoryMonitor: self)
}
// Define a cancel handler to ensure the directory is closed when the source is cancelled.
directoryMonitorSource?.setCancelHandler{
close(self.monitoredDirectoryFileDescriptor)
self.monitoredDirectoryFileDescriptor = -1
self.directoryMonitorSource = nil
}
// Start monitoring the directory via the source.
directoryMonitorSource?.resume()
}
}
func stopMonitoring() {
// Stop listening for changes to the directory, if the source has been created.
if directoryMonitorSource != nil {
// Stop monitoring the directory via the source.
directoryMonitorSource?.cancel()
}
}
}