DispatchSource: Detecting Changes in Files and Folders in Swift

Monitor all your OS-related events

Bruno Rocha
Dec 1, 2020 · 6 min read
Website logo
Website logo
Photo by the author.

The DispatchSource family of types from GCD contains a series of objects that are capable of monitoring OS-related events. In this article, we'll see how to use DispatchSource to monitor a log file and create a debug-only view that shows the app's latest logs.

Context: File Logging in Swift

While every app will print debug logs to the developer console, it’s good practice to save these logs somewhere. OSLog automatically saves your logs to the system, but I find that maintaining your own log file (like MyApp-2020-11-24T14:23:42.log) is an additional good practice. If your app receives a bug report from an external beta tester, you may find retrieving and inspecting your own log file easier than teaching that user how to extract and send their OSLogs. For example, if you have your own log files, you can add a debug gesture that automatically dumps these logs somewhere.

Regardless of how you generate these logs, you can save them in two main ways. The most common way to write a file is to write all of the contents at once using String.write(to:):

var logs = ["Logged in!", "Logged out!"]
logs.joined(separator: "\n").write(to: logsPath, atomically: false, encoding: .utf8)

This is fine if you’re writing all your logs at once when your app is going to close, but if you plan to continuously add content to a file, you should use FileHandler:

In the end, the difference between these two methods is that the first one is overwriting the file, while the second one is more similar to a text editor in that you’re modifying an existing file.

Monitoring File Changes

Monitoring changes in the file system is done by attaching a DispatchSource object to the file/folder in question and registering which events we'd like to be notified of. Note, though, that a DispatchSource is not necessarily restricted to file system events. It is capable of monitoring many types of OS-related events, including timers, processes, UNIX signals, and more things that are meant to be used in macOS instead of iOS itself.

In this article, we’re only going to monitor file events. To show how the process works, we are going to detect changes in a log file and display these changes in the app’s UI:

Changes displayed in app’s UI
Changes displayed in app’s UI

If you have something akin to an internal employee beta of your app, a feature like this can be very useful. If someone finds a bug, they can open this feature and potentially determine the cause of the issue on the fly without needing a developer to boot Xcode and run an actual debug build.

The first step to monitor file changes is to abstract all of it. Let’s start with a new FileMonitor class:

To create a DispatchSource that monitors the file system, we'll call the makeFileSystemObjectSource factory to get a new DispatchSourceFileSystemObject:

source = DispatchSource.makeFileSystemObjectSource(
fileDescriptor: ...,
eventMask: ...,
queue: ...
)

To fill these arguments, let’s describe what each of them represents.

fileDescriptor is an Int32 that represents a file descriptor pointing to the file/folder we want to monitor. Sounds crazy, right? Don't worry! The same FileHandle type used to write the logs can provide this information.

For eventMask, we should pass the event types that we want to be notified of. The enum of possibilities includes many cases like .rename, .delete, .write, and .extend, and for monitoring changes in files, the correct one to use depends on how you're writing to that file. If you're overwriting the file by calling String.write(to:), you should use .write. But if you're modifying the file with FileHandle, you should use .extend instead. For this tutorial, we'll use the latter.

Finally, the queue argument is the dispatch queue in which the events should be dispatched. For simplicity’s sake, we'll use the main queue.

In order to receive event notifications, we must pass an eventHandler to the dispatch source. This might seem weird since you'd normally use a delegate object for this, but the reason it works like this is probably that this is a very old Objective-C API.

source.setEventHandler {
let event = self.source.data
self.process(event: event)
}

When the event handler is triggered, the data property of the dispatch source will contain the set of events that were dispatched.

Additionally, we must provide a way to safely shut down the dispatch source. We do this by assigning a cancelHandler that closes the FileHandle whenever the source is canceled and by adding a deinit call to our class that cancels it:

To process the events, we’ll use the following method:

When readDataToEndOfFile() is called, the file handle will return everything between the column it's currently pointing at and the end of the file. This also makes it point to the end of the file, making it a great way of fetching the changes in the file. When another event is received, the file handle will already be positioned to read the newest changes.

If the concept of pointers here makes you confused, think of FileHandle as a cursor in a text editor. When we call readDataToEndOfFile(), we're copying everything that was added and moving the cursor to the end of it.

While the guard is going to be useless for this example, it's important to notice that FileSystemEvent is an OptionSet. As you can monitor and receive multiple event types to/from your dispatch source, the idea is that you should always check which events were received so you can call the correct logic for it.

To test all of this, we need to set up two final things. First, as we’re not interested in reading what’s already logged, we should move the file handler’s pointer to the end of the file as soon as we create it. Finally, to wrap it up, we can start the dispatch source by calling source.resume():

fileHandle.seekToEndOfFile()
source.resume()

Here’s a simple ViewController that you can use to test this:

After running this app, you should see "Detected: Woo! SwiftRocks." in the console — plus anything else you add to that file later on!

Why Doesn’t It Work When I Edit the File in an Editor?

If you try to test this by opening a text editor, adding some text, and saving the file, you’ll see that it may not work. The reason is that editors like Xcode don’t actually modify the file. Instead, they act on copies of it. When you save it, they delete the original file and replace it with the copy they were maintaining. You can confirm that this is the case by registering events like .delete and .link to your dispatch source and seeing how they get triggered when you save the file.

If you're doing this for a macOS app, one way to support text editors would be to register these cases and cancel/reboot the dispatch source when a new file is linked.

Wrapping Up: Getting It Ready for Our Debug Feature

Because making our monitor print what was just logged to a file makes no sense, we can modify our FileMonitor to work with a delegate. Here's the full FileMonitor:

From here, creating a view that displays the latest logs like in the example picture is just a matter of creating a new FileMonitor and setting the feature as the delegate.

You can make a feature like this without file logging/monitoring, but adding it to the mix would allow you to isolate the feature’s logic from the actual logging mechanics. For something that’s meant to only be used when debugging, that can be very nice in terms of architecture.

Better Programming

Advice for programmers.

By Better Programming

A weekly newsletter sent every Friday with the best articles we published that week. Code tutorials, advice, career opportunities, and more! Take a look

By signing up, you will create a Medium account if you don’t already have one. Review our Privacy Policy for more information about our privacy practices.

Check your inbox
Medium sent you an email at to complete your subscription.

Bruno Rocha

Written by

iOS Developer at Spotify | Follow me on Twitter at @rockbruno_ for more iOS tricks! Writer of https://swiftrocks.com

Better Programming

Advice for programmers.

Bruno Rocha

Written by

iOS Developer at Spotify | Follow me on Twitter at @rockbruno_ for more iOS tricks! Writer of https://swiftrocks.com

Better Programming

Advice for programmers.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store