When Swift was announced at WWDC 2014, I was very disappointed to not be able to use some of powerful C APIs such as FSEvents, to write applications in pure Swift language.
A lot of C APIs needs a C function pointer to pass a callback function when you use the API. From Swift 1.0 to Swift 1.2, it was not possible to use C function pointer in pure Swift language.
It was possible to use FSEvents by wrapping it in an Objective-C class, and by calling this class in Swift by using the bridging features offered by the language, but it was not what I attempted to accomplish.
But things change and at WWDC 2015, Apple announced the possibility to use C function pointers with Swift 2.0.
Today I’m very pleased to give you a sample code which is a good starting point to create your own FileSystemWatcher using the FSEvents API written with Swift 2.0.
import Foundation public class FileSystemWatcher { // MARK: - Initialization / Deinitialization public init(pathsToWatch: [String], sinceWhen: FSEventStreamEventId) { self.lastEventId = sinceWhen self.pathsToWatch = pathsToWatch } convenience public init(pathsToWatch: [String]) { self.init(pathsToWatch: pathsToWatch, sinceWhen: FSEventStreamEventId(kFSEventStreamEventIdSinceNow)) } deinit { stop() } // MARK: - Private Properties private let eventCallback: FSEventStreamCallback = { (stream: ConstFSEventStreamRef, contextInfo: UnsafeMutablePointer<Void>, numEvents: Int, eventPaths: UnsafeMutablePointer<Void>, eventFlags: UnsafePointer<FSEventStreamEventFlags>, eventIds: UnsafePointer<FSEventStreamEventId>) in print("***** FSEventCallback Fired *****", appendNewline: true) let fileSystemWatcher: FileSystemWatcher = unsafeBitCast(contextInfo, FileSystemWatcher.self) let paths = unsafeBitCast(eventPaths, NSArray.self) as! [String] for index in 0..<numEvents { fileSystemWatcher.processEvent(eventIds[index], eventPath: paths[index], eventFlags: eventFlags[index]) } fileSystemWatcher.lastEventId = eventIds[numEvents - 1] } private let pathsToWatch: [String] private var started = false private var streamRef: FSEventStreamRef! // MARK: - Private Methods private func processEvent(eventId: FSEventStreamEventId, eventPath: String, eventFlags: FSEventStreamEventFlags) { print("\t\(eventId) - \(eventFlags) - \(eventPath)", appendNewline: true) } // MARK: - Pubic Properties public private(set) var lastEventId: FSEventStreamEventId // MARK: - Pubic Methods public func start() { guard started == false else { return } var context = FSEventStreamContext(version: 0, info: nil, retain: nil, release: nil, copyDescription: nil) context.info = UnsafeMutablePointer<Void>(unsafeAddressOf(self)) let flags = UInt32(kFSEventStreamCreateFlagUseCFTypes | kFSEventStreamCreateFlagFileEvents) streamRef = FSEventStreamCreate(kCFAllocatorDefault, eventCallback, &context, pathsToWatch, lastEventId, 0, flags) FSEventStreamScheduleWithRunLoop(streamRef, CFRunLoopGetMain(), kCFRunLoopDefaultMode) FSEventStreamStart(streamRef) started = true } public func stop() { guard started == true else { return } FSEventStreamStop(streamRef) FSEventStreamInvalidate(streamRef) FSEventStreamRelease(streamRef) streamRef = nil started = false } }
I hope this will help you in your future applications and feel free to share this article if you found it helpful 😉
Reblogged this on SutoCom Solutions.
LikeLike
Nice work. When I run this I eventually get EXC_BAD_ACCESS in libswiftCore.dylib`_swift_release_dealloc: once I Alt-Tab from the app to the Finder. Any thought?
LikeLiked by 1 person
The culprit seems to be in
let fileSystemWatcher = unsafeBitCast(contextInfo, FileSystemWatcher.self)
When I comment this (and the depending part below) I don’t get the above error any more.
LikeLiked by 1 person
What’s your code to instantiate the FileSystemWatcher object and where your instance was declared ?
Regarding the errror message, it seems that when the callback closure is called then when the code try to get the instance from the context, the object was deallocated.
In my own application, my code which works looks like this :
class MainController: NSViewController {
let fileSystemWatcher = FileSystemWatcher(…)
}
The instance has the same liftetime as the main application view controller.
LikeLike
Huu. I don’t know what happened. I put this aside for the moment and worked on other stuff. Now when I started it once again I had no more issues. I guess you know what I mean 😉 I’ll dive into this more seriously the next days and will come back with good (or bad) details. Thanks for the fast response.
LikeLike
Hi Thomas & Stephane, I am getting same EXC_BAD_ACCESS error. Any idea how can i call closure from eventCallback.
LikeLiked by 1 person
Sorry, no idea. As told the error went away just after some time doing other stuff.
LikeLiked by 1 person
Pingback: Developing a FileSystemWatcher for OS X by using FSEvents with Swift 2.0 | Dinesh Ram Kali.
Hi Stephane, I am setting closer in start function and want to call it in eventCallback like fileSystemWatcher.eventHandler(eventPath:eventPath). But i am getting nil exception. Looks like object is changed in eventCallback. Any idea how can i do that.
LikeLike
Hi Bipin. Without a sample of what you try to do, it’s pretty difficult to help you and see why you get an exception.
LikeLike
Thanks Stephane for quick response, Here is my code:
Add some stuff to FileSystemWatcher.swift class
// created eventHandler property
public var eventHandler: ((eventPath: String)->())?
// setting up eventHandler property
func start(handler: (eventPath: String) -> ()) {
self.eventHandler = handler
// other code as it is
}
private let eventCallback: FSEventStreamCallback = { (stream: ConstFSEventStreamRef, contextInfo: UnsafeMutablePointer, numEvents: Int, eventPaths: UnsafeMutablePointer, eventFlags: UnsafePointer, eventIds: UnsafePointer) in
print(“***** FSEventCallback Fired *****”)
let fileSystemWatcher = unsafeBitCast(contextInfo, FileSystemWatcher.self)
let paths = unsafeBitCast(eventPaths, NSArray.self) as! [String]
for index in 0..<numEvents {
fileSystemWatcher.processEvent(eventIds[index], eventPath: paths[index], eventFlags: eventFlags[index])
// Calling eventHandler closure here – Getting error here
fileSystemWatcher.eventHandler!(eventPath:paths[index])
}
fileSystemWatcher.lastEventId = eventIds[numEvents – 1]
}
I hope this will help you to figure out issue.
LikeLike
Which version of Swift are you using ? The code of this article was written in Swift 2.0. I’ve just tested and it also crashes on my machine with Swift 2.1 (same error as yours).
LikeLike
I found the problem and there was a bug in the code.
I updated the article and the problem was corrected with these 2 lines :
var context = FSEventStreamContext(version: 0, info: nil, retain: nil, release: nil, copyDescription: nil)
context.info = UnsafeMutablePointer(unsafeAddressOf(self))
Now it should work as expected 😉
LikeLiked by 1 person
I am using Swift version 2.0 and xCode 7.
LikeLike
Yup, it’s working fine now. Great thanks @Stephane for quick help.
LikeLike
Cant get this to work. Could you write some example code how to init this instance? I tried:
let fileSystemWatcher = FileSystemWatcher(pathsToWatch: [“~/Desktop/”.tildePath],sinceWhen: 0)
fileSystemWatcher.start()
LikeLike
Hello, all works fine, but I have a problem to convert eventFlag in
private func processEvent(eventId: FSEventStreamEventId, eventPath: String, eventFlag: FSEventStreamEventFlags) {
print(“\t\(eventId) – \(eventFlag)) – \(eventPath)”)
}
from Int value like 107776 to something like that as it described in documentation
kFSEventStreamEventFlagItemCreated = 0x00000100
Thanks!
LikeLike
Thanks for sharing the wrapper! It demonstrates very well how to use a C API (which I never tried before) and it works like a charm. I’ve converted the code to Swift 3 here:
https://gist.github.com/DivineDominion/56e56f3db43216d9eaab300d3b9f049a
LikeLike
Hello, this code does not compile anymore with Swift 3.2 . and XCode 9.. any idea on how to fix it? Requested Fix make it crash… any idea on how to fix it? Thanks
LikeLike
The fixes to my code are trivial once you let Xcode 9 do the migration for you. There are some parameters that used to be optional and now aren’t. My project has changed heavily so I cannot paste a 1:1 upgrade, though. I performed the changes manually in the gist:
FolderContentMonitor.swift
hosted with ❤ by GitHub
LikeLike