Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
FileWatcher / FileListener
import Foundation
fileprivate let queue = DispatchQueue(label: "FileMonitorQueue", target: .main)
enum FileWatcherError: Error {
case directoryOpenFailed(Int32)
case directoryURLInvalid(URL)
}
final class FileWatcher {
let fsSource: DispatchSourceFileSystemObject
var readSource: DispatchSourceRead!
init(in directory: URL, filename: String, errorHandler: ((Error) -> Void)?) throws {
let fileDescriptor: Int32 = try directory.withUnsafeFileSystemRepresentation { path in
guard let path = path else { throw FileWatcherError.directoryURLInvalid(directory) }
let result = open(path, O_EVTONLY)
guard result >= 0 else { throw FileWatcherError.directoryOpenFailed(errno) }
return result
}
fsSource = DispatchSource.makeFileSystemObjectSource(
fileDescriptor: fileDescriptor,
eventMask: .write,
queue: queue
)
fsSource.setEventHandler { [unowned self] in
do {
let fileHandle = try FileHandle(
forReadingFrom: directory.appendingPathComponent(filename)
)
self.readSource = DispatchSource.makeReadSource(fileDescriptor: fileHandle.fileDescriptor, queue: queue)
self.readSource.setEventHandler {
fileHandle.readToEndOfFileInBackgroundAndNotify()
}
self.readSource.resume()
self.readSource.setCancelHandler {
fileHandle.closeFile()
}
} catch {
errorHandler?(error)
}
}
fsSource.setCancelHandler {
close(fileDescriptor)
}
fsSource.resume()
}
deinit {
fsSource.cancel()
}
}
final class FileListener {
init() {
NotificationCenter.default.addObserver(
self,
selector: #selector(handler),
name: .NSFileHandleReadToEndOfFileCompletion,
object: nil
)
}
@objc func handler() {
/* file is available for reading here */
}
}
@DevyV

This comment has been minimized.

Copy link

@DevyV DevyV commented Feb 19, 2020

Hello Sid,

I am just now learning Swift 5 on macOS 10.14.6 with Xcode 11.3.1.

I would like to create a simple GUI app that monitors a file for changes (e.g. "/Uers/Devy/Documents/test.txt").
I found this code of yours, but being a beginner in Swift coding, I just do not know how to use in a Cocoa app.
I have a Label on the GUI and I wish to update the label whenever the file is changed with the text from the file.

So far, I coded this, however, right here I do not know what to put as "errorHandler":

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        // Insert code here to initialize your application
        
        var fw: FileWatcher
        fw = FileWatcher(in: URL(fileURLWithPath: "/Users/Devy/Documents/", isDirectory: true), filename: "test.txt", errorHandler: <???>)        
    }

And I do not know how to read from the file and show the content on the GUI...

Would you be so kind and help me achieve this, please?
Many thanks in advance!
Devy

@sidraval

This comment has been minimized.

Copy link
Owner Author

@sidraval sidraval commented Feb 19, 2020

@DevyV Not sure if you've seen it, but I wrote an accompanying blog post that might be helpful https://thoughtbot.com/blog/waiting-for-file-write-completion-on-ios

errorHandler is an optional callback function that handles any errors that might occur during file reading.

You'll want to listen to notifications for .NSFileHandleReadToEndOfFileCompletion, and update your label in the notification handler function. Line 62 shows an example class that calls the handler function when it receives a notification.

@DevyV

This comment has been minimized.

Copy link

@DevyV DevyV commented May 6, 2020

Hello Sid,

Thanks for your help, I managed to make it work :)

Actually, I am facing a strange issue now:
as long as the monitored file is being edited and saved by a text editor (e.g. SubEthaEdit) Filewatcher can detect changes.
But when I change the contents of the monitored file with AppleScript (see example below) then Filewatcher does NOT detect the change.

Why? Do you have any idea how to fix it?

Here is my AppleScript used to change the contents of the file:

set documentsPath to POSIX path of (path to documents folder from user domain)
set my_file to (documentsPath & "test_file.txt")
set newContent to "new text"

set fileHandle to open for access my_file with write permission
set eof of fileHandle to 0 -- make sure to overwrite the whole contents in the file
write ((newContent as string) & return) to fileHandle --- starting at eof
close access fileHandle

@DevyV

This comment has been minimized.

Copy link

@DevyV DevyV commented May 6, 2020

Sid,

I solved it myself :)
I needed to change line 26 from:

eventMask: .write,

to

eventMask: .all,

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment